Compare commits
306
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9021370e39 | ||
|
|
dff0a66c05 | ||
|
|
849b3e060f | ||
|
|
29c6d269a2 | ||
|
|
725debaebe | ||
|
|
7507163422 | ||
|
|
4cb9893fd9 | ||
|
|
6b4fba902b | ||
|
|
c1dc538627 | ||
|
|
6f262c8522 | ||
|
|
d8cf932672 | ||
|
|
42422b3462 | ||
|
|
c51f723133 | ||
|
|
7270a1c1ee | ||
|
|
67b9fe0555 | ||
|
|
887e4288ae | ||
|
|
1c59b34ac4 | ||
|
|
ea2ba8dfcf | ||
|
|
78a1b9b74d | ||
|
|
b99cfc74e9 | ||
|
|
3d6efa4b0a | ||
|
|
b998c47ac7 | ||
|
|
fc7848f00b | ||
|
|
125c562df5 | ||
|
|
f748147dcc | ||
|
|
77c23f8109 | ||
|
|
b98c56938f | ||
|
|
23308cd5db | ||
|
|
9d9d3aa7a3 | ||
|
|
3ddfd10678 | ||
|
|
7c75ffa815 | ||
|
|
4e4b22314a | ||
|
|
4e6b570cba | ||
|
|
b12d9ba969 | ||
|
|
83df95f0da | ||
|
|
3c5caae93f | ||
|
|
b41460c569 | ||
|
|
9ff02de48b | ||
|
|
14cbbacc1b | ||
|
|
1cadf3488d | ||
|
|
c78f411014 | ||
|
|
ab2db14bc8 | ||
|
|
36ff8e4917 | ||
|
|
f42c49d221 | ||
|
|
114f8fbfa8 | ||
|
|
8bbf03fdb4 | ||
|
|
5b57982b41 | ||
|
|
ecf7a5b13a | ||
|
|
f68951fab7 | ||
|
|
23eec5187d | ||
|
|
caa3e10648 | ||
|
|
4bc0b80eea | ||
|
|
959a7dfa85 | ||
|
|
9e793b6748 | ||
|
|
c75e057e83 | ||
|
|
6fa4aa6f75 | ||
|
|
89da167404 | ||
|
|
f2520e35fe | ||
|
|
aaaaea4dd1 | ||
|
|
ec67d97809 | ||
|
|
f6118ea4a7 | ||
|
|
d257f676ec | ||
|
|
58f4a9df57 | ||
|
|
9d91ce62d3 | ||
|
|
511ca99cba | ||
|
|
465701810b | ||
|
|
c8c0ed58df | ||
|
|
cfe3c10cec | ||
|
|
014509bd64 | ||
|
|
f5d5d34eda | ||
|
|
64c8bec614 | ||
|
|
1b765035de | ||
|
|
93f46bde6a | ||
|
|
4a09fcf06f | ||
|
|
12a2ae1326 | ||
|
|
325241d56a | ||
|
|
95664fea1f | ||
|
|
e2b43f4b06 | ||
|
|
845c8a2a40 | ||
|
|
306f8b6cb4 | ||
|
|
a704933a35 | ||
|
|
8bd09c3726 | ||
|
|
6240364067 | ||
|
|
9d37cfccd0 | ||
|
|
125efd9436 | ||
|
|
6701fd39d8 | ||
|
|
d4edf96f83 | ||
|
|
a2555a6c40 | ||
|
|
c01a6ce16b | ||
|
|
ab38afa105 | ||
|
|
ba3c2e3489 | ||
|
|
92a2cc2d9a | ||
|
|
0842739197 | ||
|
|
f09a66c083 | ||
|
|
88984365fe | ||
|
|
469b0c257e | ||
|
|
a94f59eb53 | ||
|
|
eb1fbe90d1 | ||
|
|
b5b7498041 | ||
|
|
828b41197b | ||
|
|
b0fed670b9 | ||
|
|
f13d177615 | ||
|
|
fcce28c9c4 | ||
|
|
e8e9d4e3cd | ||
|
|
59ef398452 | ||
|
|
80e39845ce | ||
|
|
cbab673084 | ||
|
|
f27df12e64 | ||
|
|
bdcb3f6203 | ||
|
|
f619b20e2a | ||
|
|
69aaee3fc4 | ||
|
|
7140895c58 | ||
|
|
190cef875e | ||
|
|
7aef8570a5 | ||
|
|
721a3ad8f2 | ||
|
|
1b1c30bff0 | ||
|
|
8c23087f89 | ||
|
|
dae2413ed1 | ||
|
|
cd9792193f | ||
|
|
75dbd1e498 | ||
|
|
a374009835 | ||
|
|
9a9501635f | ||
|
|
e00600c955 | ||
|
|
141fea542c | ||
|
|
ed4450778d | ||
|
|
41d59b1df0 | ||
|
|
fd104c09da | ||
|
|
6dbed91426 | ||
|
|
cb43f17d75 | ||
|
|
5ebf278ad3 | ||
|
|
7f782533b8 | ||
|
|
1790c1beae | ||
|
|
09e1c382d3 | ||
|
|
ceb2270ee5 | ||
|
|
986a3c81f3 | ||
|
|
11d6bbb916 | ||
|
|
b02f6318a7 | ||
|
|
c1435e74e8 | ||
|
|
b30da661e5 | ||
|
|
3a22dddc82 | ||
|
|
a19a17d4dc | ||
|
|
bff6ef92ce | ||
|
|
263572eb66 | ||
|
|
3a97a928de | ||
|
|
0322414de1 | ||
|
|
1c75e86863 | ||
|
|
0ae71f6367 | ||
|
|
af7c38695a | ||
|
|
c741c22876 | ||
|
|
06b9b12eeb | ||
|
|
daf7078f98 | ||
|
|
c2db57ecb3 | ||
|
|
0336c6a8f8 | ||
|
|
22d3fa9565 | ||
|
|
9d81973b3a | ||
|
|
fe8eac3dc6 | ||
|
|
5fc315201a | ||
|
|
a9d3d5568b | ||
|
|
f88dbdaeba | ||
|
|
48028914de | ||
|
|
fcdb70d246 | ||
|
|
e8589bfa05 | ||
|
|
854ffbf26a | ||
|
|
ca109f703c | ||
|
|
82d5a7ea51 | ||
|
|
79ead7b826 | ||
|
|
6e360b2bb1 | ||
|
|
1aaa5b649f | ||
|
|
435f3b9831 | ||
|
|
bcf0bb5525 | ||
|
|
322aae37dc | ||
|
|
45cb4028b8 | ||
|
|
151d9b7375 | ||
|
|
d393c861d7 | ||
|
|
02c4b2e7e1 | ||
|
|
77959d6d94 | ||
|
|
ad832c4d2d | ||
|
|
0ce2aec139 | ||
|
|
d3761b23ec | ||
|
|
cdbd1a4c27 | ||
|
|
2d2e569595 | ||
|
|
391373b304 | ||
|
|
a030f46b4d | ||
|
|
f6dc22f241 | ||
|
|
220638e525 | ||
|
|
24b1aac1be | ||
|
|
c4608f4258 | ||
|
|
ca19ea62a4 | ||
|
|
2d8f4d8289 | ||
|
|
24ca9d9617 | ||
|
|
41aa1839b7 | ||
|
|
40354e87a0 | ||
|
|
0fc74c3f82 | ||
|
|
7b150eb1af | ||
|
|
f8db8eb3b6 | ||
|
|
a34b6dec67 | ||
|
|
bded3eb156 | ||
|
|
9ca73934ce | ||
|
|
e02b429523 | ||
|
|
22e4b7dabe | ||
|
|
130b5acf84 | ||
|
|
d0fa303c88 | ||
|
|
dff0746450 | ||
|
|
815dfd9a8e | ||
|
|
b0ea0566a5 | ||
|
|
0167900415 | ||
|
|
9279ba2aa9 | ||
|
|
2abfc3e843 | ||
|
|
4ff83585b4 | ||
|
|
3cf2b5e093 | ||
|
|
6a7eff6501 | ||
|
|
3805a41d9f | ||
|
|
f8c78fb499 | ||
|
|
3314ff85ed | ||
|
|
67b24eb89a | ||
|
|
d416e42925 | ||
|
|
17655849ae | ||
|
|
d9e3761c7b | ||
|
|
382db80584 | ||
|
|
695dc96340 | ||
|
|
8b1851b647 | ||
|
|
eeaa9cdf6f | ||
|
|
5cd59c049e | ||
|
|
5d86e0ac9a | ||
|
|
d9136e611f | ||
|
|
5ca1aca3c0 | ||
|
|
08632ec9ce | ||
|
|
45ba46e637 | ||
|
|
e51740ca89 | ||
|
|
eb7d7d9601 | ||
|
|
c4b70f963c | ||
|
|
b73d122b15 | ||
|
|
db072b9dea | ||
|
|
5c56c903e8 | ||
|
|
0758fa66d3 | ||
|
|
6edb89a606 | ||
|
|
7f15827791 | ||
|
|
93d498aafb | ||
|
|
b8da6e2197 | ||
|
|
4f00aac7a2 | ||
|
|
7dd32a0dad | ||
|
|
eb8f0f6437 | ||
|
|
8fcd579a69 | ||
|
|
6479423c6c | ||
|
|
be1a5bf92f | ||
|
|
2788ab58ea | ||
|
|
31de65f155 | ||
|
|
fd2e4ed9a4 | ||
|
|
b029c32744 | ||
|
|
df88be56e9 | ||
|
|
816f09b002 | ||
|
|
507f350ec2 | ||
|
|
7067c05c6b | ||
|
|
338a10c8a2 | ||
|
|
44beddac18 | ||
|
|
534b5ad6e0 | ||
|
|
04ad77ad42 | ||
|
|
476042a6e6 | ||
|
|
d40787499e | ||
|
|
9e53ebfb05 | ||
|
|
0455210598 | ||
|
|
4d66d8e0c4 | ||
|
|
020b6d9855 | ||
|
|
1d9efabfd6 | ||
|
|
b8fc4934e2 | ||
|
|
37743f78d5 | ||
|
|
27cddaac70 | ||
|
|
9d828a254c | ||
|
|
8bc4544eeb | ||
|
|
cec801c39c | ||
|
|
2abf195391 | ||
|
|
e6469aea5f | ||
|
|
470ab17813 | ||
|
|
42e444c0e0 | ||
|
|
2baae9ea45 | ||
|
|
789724a6a3 | ||
|
|
e53439141a | ||
|
|
d5ec39113e | ||
|
|
58117b3daf | ||
|
|
558c190512 | ||
|
|
9e124ce5cc | ||
|
|
396824d38c | ||
|
|
cc860c3d03 | ||
|
|
6a5256e9b1 | ||
|
|
00c2c2cc29 | ||
|
|
4775bda51c | ||
|
|
38fed26742 | ||
|
|
99235fbc80 | ||
|
|
feef686767 | ||
|
|
1e00f657a7 | ||
|
|
672fca6dbf | ||
|
|
bc85779272 | ||
|
|
669fb008d1 | ||
|
|
7281241edd | ||
|
|
9394964688 | ||
|
|
d8c80c24fc | ||
|
|
12f7e65d0a | ||
|
|
d31cb60a17 | ||
|
|
1b8bae3d25 | ||
|
|
598b2e4235 | ||
|
|
569249a6f4 | ||
|
|
754d441111 | ||
|
|
4ca9cc21fe | ||
|
|
2fc3e735c5 | ||
|
|
ca60d99e90 | ||
|
|
f0445683ca |
@@ -0,0 +1,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- fronzbot
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
daysUntilStale: 30
|
||||
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
@@ -8,7 +8,8 @@ daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- Priority
|
||||
- bug
|
||||
- Help Wanted
|
||||
- help wanted
|
||||
- feature request
|
||||
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: true
|
||||
|
||||
@@ -33,8 +33,9 @@ jobs:
|
||||
run: |
|
||||
tox -r -e cov
|
||||
- name: Codecov
|
||||
uses: codecov/codecov-action@v1.0.6
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: unittests
|
||||
file: ./coverage.xml
|
||||
name: blinkpy
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.8]
|
||||
python-version: [3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.7'
|
||||
python-version: '3.8'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
@@ -10,11 +10,11 @@ jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
strategy:
|
||||
max-parallel: 3
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
platform:
|
||||
- ubuntu-latest
|
||||
python-version: [3.6, 3.7, 3.8]
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -14,3 +14,4 @@ build/*
|
||||
docs/_build
|
||||
*.log
|
||||
venv
|
||||
.session*
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
image: python
|
||||
|
||||
stages:
|
||||
- test
|
||||
|
||||
before_script:
|
||||
- curl -O https://bootstrap.pypa.io/get-pip.py
|
||||
- python get-pip.py
|
||||
- pip install tox
|
||||
|
||||
python35:
|
||||
image: python:3.5
|
||||
stage: test
|
||||
script: tox -e py35
|
||||
|
||||
python36:
|
||||
image: python:3.6
|
||||
stage: test
|
||||
script: tox -e py36
|
||||
|
||||
lint:
|
||||
image: python:3.6
|
||||
stage: test
|
||||
script: tox -e lint
|
||||
|
||||
@@ -8,10 +8,15 @@ repos:
|
||||
- --quiet
|
||||
files: ^((blinkpy|tests)/.+)?[^/]+\.py$
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.8.1
|
||||
rev: 3.8.3
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- flake8-docstrings==1.5.0
|
||||
- pydocstyle==5.0.2
|
||||
files: ^(homeassistant|tests)/.+\.py$
|
||||
- pydocstyle==5.1.1
|
||||
files: ^(blinkpy|tests)/.+\.py$
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks-markup
|
||||
rev: v1.0.0
|
||||
hooks:
|
||||
- id: rst-linter
|
||||
files: /.+\.rst$
|
||||
|
||||
+138
-32
@@ -1,10 +1,116 @@
|
||||
=========
|
||||
Changelog
|
||||
-----------
|
||||
=========
|
||||
|
||||
A list of changes between each release
|
||||
|
||||
0.17.1 (2021-02-18)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Add delay parameter to Blink.download_videos method in order to throttle API during video retrieval (`#437 <https://github.com/fronzbot/blinkpy/pull/437>`__)
|
||||
- Bump pylint to 2.6.2
|
||||
|
||||
|
||||
0.17.0 (2021-02-15)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes:**
|
||||
- Fix video downloading bug (`#424 <https://github.com/fronzbot/blinkpy/pull/424>`__)
|
||||
- Fix repeated authorization email bug (`#432 <https://github.com/fronzbot/blinkpy/pull/432>`__ and `#428 <https://github.com/fronzbot/blinkpy/pull/428>`__)
|
||||
|
||||
**New Features:**
|
||||
- Add logout method (`#429 <https://github.com/fronzbot/blinkpy/pull/429>`__)
|
||||
- Add camera record method (`#430 <https://github.com/fronzbot/blinkpy/pull/430>`__)
|
||||
|
||||
**Other:**
|
||||
- Add debug script to main repo to help with general debug
|
||||
- Upgrade login endpoint from v4 to v5
|
||||
- Add python 3.9 support
|
||||
- Bump coverage to 5.4
|
||||
- Bump pytest to 6.2.2
|
||||
- Bump pytest-cov to 2.11.1
|
||||
- Bump pygments to 2.8.0
|
||||
- Bump pre-commit to 2.10.1
|
||||
- Bump restructuredtext-lint to 1.3.2
|
||||
|
||||
|
||||
0.16.4 (2020-11-22)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes:**
|
||||
|
||||
- Updated liveview endpoint (`#389 <https://github.com/fronzbot/blinkpy/pull/389>`__)
|
||||
- Fixed mini thumbnail not updating (`#388 <https://github.com/fronzbot/blinkpy/pull/388>`__)
|
||||
- Add exception catch to prevent NoneType error on refresh, added test to check behavior as well (`#401 <https://github.com/fronzbot/blinkpy/pull/401>`__)
|
||||
- Unrelated: had to add two force methods to refresh for testing purposes. Should not change normal usage.
|
||||
- Fix malformed stream url (`#395 <https://github.com/fronzbot/blinkpy/pull/395>`__)
|
||||
|
||||
**All:**
|
||||
|
||||
- Moved testtools to requirements_test.txt (`#387 <https://github.com/fronzbot/blinkpy/pull/387>`__)
|
||||
- Bumped pytest to 6.1.1
|
||||
- Bumped flake8 to 3.8.4
|
||||
- Fixed README spelling ((`#381 <https://github.com/fronzbot/blinkpy/pull/381>`__) via @rohitsud)
|
||||
- Bumped pygments to 2.7.1
|
||||
- Bumped coverage to 5.3
|
||||
- Bumped pydocstyle to 5.1.1
|
||||
- Bumped pre-commit to 2.7.1
|
||||
- Bumped pylint to 2.6.0
|
||||
- Bumped pytest-cov to 2.10.1
|
||||
|
||||
|
||||
0.16.3 (2020-08-02)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Add user-agent to all headers
|
||||
|
||||
0.16.2 (2020-08-01)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Add user-agent to header at login
|
||||
- Remove extra data parameters at login (not-needed)
|
||||
- Bump pytest to 6.0.1
|
||||
|
||||
|
||||
0.16.1 (2020-07-29)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Unpin requirements, set minimum version instead
|
||||
- Bump coverage to 5.2.1
|
||||
- Bump pytest to 6.0.0
|
||||
|
||||
|
||||
0.16.0 (2020-07-20)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Breaking Changes:**
|
||||
|
||||
- Add arm property to camera, deprecate motion enable method (`#273 <https://github.com/fronzbot/blinkpy/pull/273>`__)
|
||||
- Complete refactoring of auth logic (breaks all pre-0.16.0 setups!) (`#261 <https://github.com/fronzbot/blinkpy/pull/261>`__)
|
||||
|
||||
**New Features:**
|
||||
|
||||
- Add is_errored property to Auth class (`#275 <https://github.com/fronzbot/blinkpy/pull/275>`__)
|
||||
- Add new endpoint to get user infor (`#280 <https://github.com/fronzbot/blinkpy/pull/280>`__)
|
||||
- Add get_liveview command to camera module (`#289 <https://github.com/fronzbot/blinkpy/pull/289>`__)
|
||||
- Add blink Mini Camera support (`#290 <https://github.com/fronzbot/blinkpy/pull/290>`__)
|
||||
- Add option to skip homescreen check (`#305 <https://github.com/fronzbot/blinkpy/pull/305>`__)
|
||||
- Add different timeout for video and image retrieval (`#323 <https://github.com/fronzbot/blinkpy/pull/323>`__)
|
||||
- Modifiy session to use HTTPAdapter and handle retries (`#324 <https://github.com/fronzbot/blinkpy/pull/324>`__)
|
||||
- Add retry option overrides (`#339 <https://github.com/fronzbot/blinkpy/pull/339>`__)
|
||||
|
||||
**All changes:**
|
||||
|
||||
Please see the change list in the (`Release Notes <https://github.com/fronzbot/releases/tag/v0.16.0>`__)
|
||||
|
||||
|
||||
0.15.1 (2020-07-11)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Bugfix: remove "Host" from auth header (`#330 <https://github.com/fronzbot/blinkpy/pull/330>`__)
|
||||
|
||||
|
||||
0.15.0 (2020-05-08)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
**Breaking Changes:**
|
||||
|
||||
- Removed support for Python 3.5 (3.6 is now the minimum supported version)
|
||||
@@ -29,7 +135,7 @@ This can be used by instantiating the Blink class with the ``device_id`` paramet
|
||||
- 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
|
||||
@@ -37,18 +143,18 @@ This can be used by instantiating the Blink class with the ``device_id`` paramet
|
||||
- Log response message from server if not attempting a re-authorization
|
||||
|
||||
0.14.2 (2019-10-12)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Update dependencies
|
||||
- Dockerize `(@3ch01c <https://github.com/fronzbot/blinkpy/pull/198>__)`
|
||||
|
||||
0.14.1 (2019-06-20)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Fix timeout problems blocking blinkpy startup
|
||||
- Updated login urls using ``rest-region`` subdomain
|
||||
- Removed deprecated thumbanil recovery from homescreen
|
||||
|
||||
0.14.0 (2019-05-23)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
**Breaking Changes:**
|
||||
|
||||
- ``BlinkCamera.battery`` no longer reports a percentage, instead it returns a string representing the state of the battery.
|
||||
@@ -70,11 +176,11 @@ This can be used by instantiating the Blink class with the ``device_id`` paramet
|
||||
|
||||
|
||||
0.13.1 (2019-03-01)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Remove throttle decorator from network status request
|
||||
|
||||
0.13.0 (2019-03-01)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
**Breaking change:**
|
||||
Wifi status reported in dBm again, instead of bars (which is great). Also, the old ``get_camera_info`` method has changed and requires a ``camera_id`` parameter.
|
||||
|
||||
@@ -99,11 +205,11 @@ Wifi status reported in dBm again, instead of bars (which is great). Also, the
|
||||
|
||||
|
||||
0.12.1 (2019-01-31)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Remove logging improvements since they were incompatible with home-assistant logging
|
||||
|
||||
0.12.0 (2019-01-31)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Fix video api endpoint, re-enables motion detection
|
||||
- Add improved logging capability
|
||||
- Add download video method
|
||||
@@ -111,22 +217,22 @@ Wifi status reported in dBm again, instead of bars (which is great). Also, the
|
||||
|
||||
|
||||
0.11.2 (2019-01-23)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Hotfix to prevent platform from stalling due to API change
|
||||
- Motion detection and video recovery broken until new API endpoint discovered
|
||||
|
||||
0.11.1 (2019-01-02)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Fixed incorrect backup login url
|
||||
- Added calibrated temperature property for cameras
|
||||
|
||||
|
||||
0.11.0 (2018-11-23)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Added support for multiple sync modules
|
||||
|
||||
0.10.3 (2018-11-18)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Use networks endpoint rather than homecreen to retrieve arm/disarm status (`@md-reddevil <https://github.com/fronzbot/blinkpy/pull/119>`__)
|
||||
- Fix incorrect command status endpoint (`@md-reddevil <https://github.com/fronzbot/blinkpy/pull/118>`__)
|
||||
- Add extra debug logging
|
||||
@@ -134,18 +240,18 @@ Wifi status reported in dBm again, instead of bars (which is great). Also, the
|
||||
|
||||
|
||||
0.10.2 (2018-10-30)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Set minimum required version of the requests library to 2.20.0 due to vulnerability in earlier releases.
|
||||
- When multiple networks detected, changed log level to ``warning`` from ``error``
|
||||
|
||||
|
||||
0.10.1 (2018-10-18)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Fix re-authorization bug (fixes `#101 <https://github.com/fronzbot/blinkpy/issues/#101>`__)
|
||||
- Log an error if saving video that doesn't exist
|
||||
|
||||
0.10.0 (2018-10-16)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Moved all API calls to own module for easier maintainability
|
||||
- Added network ids to sync module and cameras to allow for multi-network use
|
||||
- Removed dependency on video existance prior to camera setup (fixes `#93 <https://github.com/fronzbot/blinkpy/issues/#93>`__)
|
||||
@@ -157,7 +263,7 @@ Wifi status reported in dBm again, instead of bars (which is great). Also, the
|
||||
- Check if retrieved clip is "None" prior to storing in cache
|
||||
|
||||
0.9.0 (2018-09-27)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Complete code refactoring to enable future multi-sync module support
|
||||
- Add image and video caching to the cameras
|
||||
- Add internal throttling of system refresh
|
||||
@@ -168,7 +274,7 @@ Wifi status reported in dBm again, instead of bars (which is great). Also, the
|
||||
|
||||
|
||||
0.8.1 (2018-09-24)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Update requirements_test.txt
|
||||
- Update linter versions
|
||||
- Fix pylint warnings
|
||||
@@ -184,18 +290,18 @@ Wifi status reported in dBm again, instead of bars (which is great). Also, the
|
||||
- Reset the value every system refresh
|
||||
|
||||
0.8.0 (2018-05-21)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Added support for battery voltage level (fixes `#64 <https://github.com/fronzbot/blinkpy/issues/64>`__)
|
||||
- Added motion detection per camera
|
||||
- Added fully accessible camera configuration dict
|
||||
- Added celcius property to camera (fixes `#60 <https://github.com/fronzbot/blinkpy/issues/60>`__)
|
||||
|
||||
0.7.1 (2018-05-09)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Fixed pip 10 import issue during setup (`@fronzbot <https://github.com/fronzbot/blinkpy/pull/61>`__)
|
||||
|
||||
0.7.0 (2018-02-08)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Fixed style errors for bumped pydocstring and pylint versions
|
||||
- Changed Blink.cameras dictionary to be case-insensitive (fixes `#35 <https://github.com/fronzbot/blinkpy/issues/35>`__)
|
||||
- Changed api endpoint for video extraction (fixes `#35 <https://github.com/fronzbot/blinkpy/issues/35>`__ and `#41 <https://github.com/fronzbot/blinkpy/issues/41>`__)
|
||||
@@ -209,7 +315,7 @@ Wifi status reported in dBm again, instead of bars (which is great). Also, the
|
||||
- Added ``attributes`` dictionary to camera object
|
||||
|
||||
0.6.0 (2017-05-12)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
- Removed redundent properties that only called hidden variables
|
||||
- Revised request wrapper function to be more intelligent
|
||||
- Added tests to ensure exceptions are caught and handled (100% coverage!)
|
||||
@@ -217,44 +323,44 @@ Wifi status reported in dBm again, instead of bars (which is great). Also, the
|
||||
- Added battery level string to reduce confusion with the way Blink reports battery level as integer from 0 to 3
|
||||
|
||||
0.5.2 (2017-03-12)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
- Fixed packaging mishap, same as 0.5.0 otherwise
|
||||
|
||||
0.5.0 (2017-03-12)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
- Fixed region handling problem
|
||||
- Added rest.piri subdomain as a backup if region can't be found
|
||||
- Improved the file writing function
|
||||
- Large test coverage increase
|
||||
|
||||
0.4.4 (2017-03-06)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
- Fixed bug where region id was not being set in the header
|
||||
|
||||
0.4.3 (2017-03-05)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
- Changed to bdist_wheel release
|
||||
|
||||
0.4.2 (2017-01-28)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
- Fixed inability to retrieve motion data due to Key Error
|
||||
|
||||
0.4.1 (2017-01-27)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
- Fixed refresh bug (0.3.1 did not actually fix the problem)
|
||||
- Image refresh routine added (per camera)
|
||||
- Dictionary of thumbnails per camera added
|
||||
- Improved test coverage
|
||||
|
||||
0.3.1 (2017-01-25)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
- Fixed refresh bug (Key Error)
|
||||
|
||||
0.3.0 (2017-01-25)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
- Added device id to camera lookup table
|
||||
- Added image to file method
|
||||
|
||||
0.2.0 (2017-01-21)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
- Initial release of blinkpy
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
# Contributing to blinkpy
|
||||
|
||||
Everyone is welcome to contribute to blinkpy! The process to get started is described below.
|
||||
|
||||
## Fork the Repository
|
||||
|
||||
You can do this right in github: just click the 'fork' button at the top right.
|
||||
|
||||
## Start Developing
|
||||
|
||||
1. Setup Local Repository
|
||||
```shell
|
||||
$ git clone https://github.com/<YOUR_GIT_USERNAME>/blinkpy.git
|
||||
$ cd blinkpy
|
||||
$ git remote add upstream https://github.com/fronzbot/blinkpy.git
|
||||
```
|
||||
|
||||
2. Create virtualenv and install dependencies
|
||||
```shell
|
||||
$ python -m venv venv
|
||||
$ source venv/bin/activate
|
||||
$ pip install -r requirements.txt
|
||||
$ pip install -r requirements_test.txt
|
||||
$ pre-commit install
|
||||
```
|
||||
|
||||
3. Create a Local Branch
|
||||
|
||||
First, you will want to create a new branch to hold your changes:
|
||||
``git checkout -b <your-branch-name>``
|
||||
|
||||
|
||||
4. Make changes
|
||||
|
||||
Now you can make changes to your code. It is worthwhile to test your code as you progress (see the **Testing** section)
|
||||
|
||||
5. Commit Your Changes
|
||||
|
||||
To commit changes to your branch, simply add the files you want and the commit them to the branch. After that, you can push to your fork on GitHub:
|
||||
```shell
|
||||
$ git add .
|
||||
$ git commit -m "Put your commit text here. Please be concise, but descriptive."
|
||||
$ git push origin HEAD
|
||||
```
|
||||
|
||||
6. Submit your pull request on GitHub
|
||||
|
||||
- On GitHub, navigate to the [blinkpy](https://github.com/fronzbot/blinkpy) repository.
|
||||
- In the "Branch" menu, choose the branch that contains your commits (from your fork).
|
||||
- To the right of the Branch menu, click New pull request.
|
||||
- The base branch dropdown menu should read `dev`. Use the compare branch drop-down menu to choose the branch you made your changes in.
|
||||
- Type a title and complete the provided description for your pull request.
|
||||
- Click Create pull request.
|
||||
- More detailed instructions can be found here: [Creating a Pull Request](https://help.github.com/articles/creating-a-pull-request/)
|
||||
|
||||
7. Prior to merge approval
|
||||
|
||||
Finally, the `blinkpy` repository uses continuous integration tools to run tests prior to merging. If there are any problems, you will see a red 'X' next to your pull request. To see what's wrong, you can find your pull request [here](https://travis- ci.org/fronzbot/blinkpy/pull_requests) and click on the failing test to see the logs. Those logs will indicate, as best as they can, what is causing that test to fail.
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
It is important to test the code to make sure your changes don't break anything major and that they pass PEP8 style conventions.
|
||||
First, you need to locally install ``tox``
|
||||
|
||||
```shell
|
||||
$ pip3 install tox
|
||||
```
|
||||
|
||||
You can then run all of the tests with the following command:
|
||||
|
||||
```shell
|
||||
$ tox
|
||||
```
|
||||
|
||||
### Tips
|
||||
|
||||
If you only want to see if you can pass the local tests, you can run `tox -e py35` (or whatever python version you have installed. Only `py35`, `py36`, and `py37` will be accepted). If you just want to check for style violations, you can run `tox -e lint`. Regardless, when you submit a pull request, your code MUST pass both the unit tests, and the linters.
|
||||
|
||||
If you need to change anything in `requirements.txt` for any reason, you'll want to regenerate the virtual envrionments used by `tox` by running with the `-r` flag: `tox -r`
|
||||
|
||||
If you want to run a single test (perhaps you only changed a small thing in one file) you can run `tox -e py35 -- tests/<testname>.py -x`. This will run the test `<testname>.py` and stop testing upon the first failure, making it easier to figure out why a particular test might be failing. The test structure mimics the library structure, so if you changed something in `sync_module.py`, the associated test file would be in `test_sync_module.py` (ie. the filename is prepended with `test_`.
|
||||
|
||||
# Catching Up With Reality
|
||||
|
||||
If your code is taking a while to develop, you may be behind the ``dev`` branch, in which case you need to catch up before creating your pull-request. To do this you can run ``git rebase`` as follows (running this on your local branch):
|
||||
|
||||
```shell
|
||||
$ git fetch upstream dev
|
||||
$ git rebase upstream/dev
|
||||
```
|
||||
|
||||
If rebase detects conflicts, repeat the following process until all changes have been resolved:
|
||||
|
||||
1. ``git status`` shows you the filw with a conflict. You will need to edit that file and resolve the lines between ``<<<< | >>>>`.
|
||||
2. Add the modified file: ``git add <file>`` or ``git add .``.
|
||||
3. Continue rebase: ``git rebase --continue``.
|
||||
4. Repeat until all conflicts resolved.
|
||||
@@ -0,0 +1,109 @@
|
||||
========================
|
||||
Contributing to blinkpy
|
||||
========================
|
||||
|
||||
Everyone is welcome to contribute to blinkpy! The process to get started is described below.
|
||||
|
||||
|
||||
Fork the Repository
|
||||
-------------------
|
||||
|
||||
You can do this right in github: just click the 'fork' button at the top right.
|
||||
|
||||
Start Developing
|
||||
-----------------
|
||||
|
||||
1. Setup Local Repository
|
||||
.. code:: bash
|
||||
|
||||
$ git clone https://github.com/<YOUR_GIT_USERNAME>/blinkpy.git
|
||||
$ cd blinkpy
|
||||
$ git remote add upstream https://github.com/fronzbot/blinkpy.git
|
||||
|
||||
2. Create virtualenv and install dependencies
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ python -m venv venv
|
||||
$ source venv/bin/activate
|
||||
$ pip install -r requirements.txt
|
||||
$ pip install -r requirements_test.txt
|
||||
$ pre-commit install
|
||||
|
||||
3. Create a Local Branch
|
||||
|
||||
First, you will want to create a new branch to hold your changes:
|
||||
``git checkout -b <your-branch-name>``
|
||||
|
||||
|
||||
4. Make changes
|
||||
|
||||
Now you can make changes to your code. It is worthwhile to test your code as you progress (see the **Testing** section)
|
||||
|
||||
5. Commit Your Changes
|
||||
|
||||
To commit changes to your branch, simply add the files you want and the commit them to the branch. After that, you can push to your fork on GitHub:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ git add .
|
||||
$ git commit
|
||||
$ git push origin HEAD
|
||||
|
||||
6. Submit your pull request on GitHub
|
||||
|
||||
- On GitHub, navigate to the `blinkpy <https://github.com/fronzbot/blinkpy>`__ repository.
|
||||
- In the "Branch" menu, choose the branch that contains your commits (from your fork).
|
||||
- To the right of the Branch menu, click New pull request.
|
||||
- The base branch dropdown menu should read ``dev``. Use the compare branch drop-down menu to choose the branch you made your changes in.
|
||||
- Type a title and complete the provided description for your pull request.
|
||||
- Click Create pull request.
|
||||
- More detailed instructions can be found here: `Creating a Pull Request` <https://help.github.com/articles/creating-a-pull-request>`__
|
||||
|
||||
7. Prior to merge approval
|
||||
|
||||
Finally, the ``blinkpy`` repository uses continuous integration tools to run tests prior to merging. If there are any problems, you will see a red 'X' next to your pull request.
|
||||
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
It is important to test the code to make sure your changes don't break anything major and that they pass PEP8 style conventions.
|
||||
First, you need to locally install ``tox``
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ pip install tox
|
||||
|
||||
|
||||
You can then run all of the tests with the following command:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ tox
|
||||
|
||||
**Tips**
|
||||
|
||||
If you only want to see if you can pass the local tests, you can run ``tox -e py37`` (or whatever python version you have installed. Only ``py36``, ``py37``, and ``py38`` will be accepted). If you just want to check for style violations, you can run ``tox -e lint``. Regardless, when you submit a pull request, your code MUST pass both the unit tests, and the linters.
|
||||
|
||||
If you need to change anything in ``requirements.txt`` for any reason, you'll want to regenerate the virtual envrionments used by ``tox`` by running with the ``-r`` flag: ``tox -r``
|
||||
|
||||
If you want to run a single test (perhaps you only changed a small thing in one file) you can run ``tox -e py37 -- tests/<testname>.py -x``. This will run the test ``<testname>.py`` and stop testing upon the first failure, making it easier to figure out why a particular test might be failing. The test structure mimics the library structure, so if you changed something in ``sync_module.py``, the associated test file would be in ``test_sync_module.py`` (ie. the filename is prepended with ``test_``.
|
||||
|
||||
|
||||
Catching Up With Reality
|
||||
-------------------------
|
||||
|
||||
If your code is taking a while to develop, you may be behind the ``dev`` branch, in which case you need to catch up before creating your pull-request. To do this you can run ``git rebase`` as follows (running this on your local branch):
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ git fetch upstream dev
|
||||
$ git rebase upstream/dev
|
||||
|
||||
If rebase detects conflicts, repeat the following process until all changes have been resolved:
|
||||
|
||||
1. ``git status`` shows you the filw with a conflict. You will need to edit that file and resolve the lines between ``<<<< | >>>>``.
|
||||
2. Add the modified file: ``git add <file>`` or ``git add .``.
|
||||
3. Continue rebase: ``git rebase --continue``.
|
||||
4. Repeat until all conflicts resolved.
|
||||
+51
-19
@@ -4,10 +4,9 @@ A Python library for the Blink Camera system (Python 3.6+)
|
||||
|
||||
Like the library? Consider buying me a cup of coffee!
|
||||
|
||||
|Donate|
|
||||
`Buy me a Coffee! <https://buymeacoffee.com/kevinfronczak>`__
|
||||
|
||||
Disclaimer:
|
||||
~~~~~~~~~~~~~~~
|
||||
**Disclaimer:**
|
||||
Published under the MIT license - See LICENSE file for more details.
|
||||
|
||||
"Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark owned by Immedia Inc., see www.blinkforhome.com for more information.
|
||||
@@ -18,11 +17,11 @@ Original protocol hacking by MattTW : https://github.com/MattTW/BlinkMonitorProt
|
||||
API calls faster than 60 seconds is not recommended as it can overwhelm Blink's servers. Please use this module responsibly.
|
||||
|
||||
Installation
|
||||
================
|
||||
``pip3 install blinkpy``
|
||||
-------------
|
||||
``pip install blinkpy``
|
||||
|
||||
Installing Development Version
|
||||
==================================
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
To install the current development version, perform the following steps. Note that the following will create a blinkpy directory in your home area:
|
||||
|
||||
.. code:: bash
|
||||
@@ -35,12 +34,12 @@ To install the current development version, perform the following steps. Note t
|
||||
$ pip3 install --upgrade dist/*.whl
|
||||
|
||||
|
||||
If you'd like to contribute to this library, please read the `contributing instructions <https://github.com/fronzbot/blinkpy/blob/dev/CONTRIBUTING.md>`__.
|
||||
If you'd like to contribute to this library, please read the `contributing instructions <https://github.com/fronzbot/blinkpy/blob/dev/CONTRIBUTING.rst>`__.
|
||||
|
||||
For more information on how to use this library, please `read the docs <https://blinkpy.readthedocs.io/en/latest/>`__.
|
||||
|
||||
Purpose
|
||||
===========
|
||||
-------
|
||||
This library was built with the intention of allowing easy communication with Blink camera systems, specifically to support the `Blink component <https://home-assistant.io/components/blink>`__ in `homeassistant <https://home-assistant.io/>`__.
|
||||
|
||||
Quick Start
|
||||
@@ -58,7 +57,7 @@ The simplest way to use this package from a terminal is to call ``Blink.start()`
|
||||
This flow will prompt you for your username and password. Once entered, if you likely will need to send a 2FA key to the blink servers (this pin is sent to your email address). When you receive this pin, enter at the prompt and the Blink library will proceed with setup.
|
||||
|
||||
Starting blink without a prompt
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
-------------------------------
|
||||
In some cases, having an interactive command-line session is not desired. In this case, you will need to set the ``Blink.auth.no_prompt`` value to ``True``. In addition, since you will not be prompted with a username and password, you must supply the login data to the blink authentication handler. This is best done by instantiating your own auth handler with a dictionary containing at least your username and password.
|
||||
|
||||
.. code:: python
|
||||
@@ -82,7 +81,7 @@ Since you will not be prompted for any 2FA pin, you must call the ``blink.auth.s
|
||||
|
||||
|
||||
Supplying credentials from file
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
--------------------------------
|
||||
Other use cases may involved loading credentials from a file. This file must be ``json`` formatted and contain a minimum of ``username`` and ``password``. A built in function in the ``blinkpy.helpers.util`` module can aid in loading this file. Note, if ``no_prompt`` is desired, a similar flow can be followed as above.
|
||||
|
||||
.. code:: python
|
||||
@@ -98,7 +97,7 @@ Other use cases may involved loading credentials from a file. This file must be
|
||||
|
||||
|
||||
Saving credentials
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
-------------------
|
||||
This library also allows you to save your credentials to use in future sessions. Saved information includes authentication tokens as well as unique ids which should allow for a more streamlined experience and limits the frequency of login requests. This data can be saved as follows (it can then be loaded by following the instructions above for supplying credentials from a file):
|
||||
|
||||
.. code:: python
|
||||
@@ -107,7 +106,7 @@ This library also allows you to save your credentials to use in future sessions.
|
||||
|
||||
|
||||
Getting cameras
|
||||
~~~~~~~~~~~~~~~~
|
||||
----------------
|
||||
Cameras are instantiated as individual ``BlinkCamera`` classes within a ``BlinkSyncModule`` instance. All of your sync modules are stored within the ``Blink.sync`` dictionary and can be accessed using the name of the sync module as the key (this is the name of your sync module in the Blink App).
|
||||
|
||||
The below code will display cameras and their available attributes:
|
||||
@@ -138,15 +137,50 @@ The ``blinkpy`` api also allows for saving images and videos to a file and snapp
|
||||
camera.image_to_file('/local/path/for/image.jpg')
|
||||
camera.video_to_file('/local/path/for/video.mp4')
|
||||
|
||||
Download videos
|
||||
~~~~~~~~~~~~~~~~
|
||||
You can also use this library to download all videos from the server. In order to do this, you must specify a ``path``. You may also specifiy a how far back in time to go to retrieve videos via the ``since=`` variable (a simple string such as ``"2017/09/21"`` is sufficient), as well as how many pages to traverse via the ``page=`` variable. Note that by default, the library will search the first ten pages which is sufficient in most use cases. Additionally, you can specidy one or more cameras via the ``camera=`` property. This can be a single string indicating the name of the camera, or a list of camera names. By default, it is set to the string ``'all'`` to grab videos from all cameras.
|
||||
|
||||
Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34am to the ``/home/blink`` directory:
|
||||
Arming Blink
|
||||
-------------
|
||||
Methods exist to arm/disarm the sync module, as well as enable/disable motion detection for individual cameras. This is done as follows:
|
||||
|
||||
.. code:: python
|
||||
|
||||
blink.download_videos('/home/blink', since='2018/07/04 09:34')
|
||||
# Arm a sync module
|
||||
blink.sync["SYNC MODULE NAME"].arm = True
|
||||
|
||||
# Disarm a sync module
|
||||
blink.sync["SYNC MODULE NAME"].disarm = False
|
||||
|
||||
# Print arm status of a sync module - a system refresh should be performed first
|
||||
blink.refresh()
|
||||
sync = blink.sync["SYNC MODULE NAME"]
|
||||
print(f"{sync.name} status: {sync.arm}")
|
||||
|
||||
Similar methods exist for individual cameras:
|
||||
|
||||
.. code:: python
|
||||
|
||||
camera = blink.cameras["SOME CAMERA NAME"]
|
||||
|
||||
# Enable motion detection on a camera
|
||||
camera.arm = True
|
||||
|
||||
# Disable motion detection on a camera
|
||||
camera.arm = False
|
||||
|
||||
# Print arm status of a sync module - a system refresh should be performed first
|
||||
blink.refresh()
|
||||
print(f"{camera.name} status: {camera.arm}")
|
||||
|
||||
|
||||
Download videos
|
||||
----------------
|
||||
You can also use this library to download all videos from the server. In order to do this, you must specify a ``path``. You may also specifiy a how far back in time to go to retrieve videos via the ``since=`` variable (a simple string such as ``"2017/09/21"`` is sufficient), as well as how many pages to traverse via the ``stop=`` variable. Note that by default, the library will search the first ten pages which is sufficient in most use cases. Additionally, you can specify one or more cameras via the ``camera=`` property. This can be a single string indicating the name of the camera, or a list of camera names. By default, it is set to the string ``'all'`` to grab videos from all cameras. If you are downloading many items, setting the ``delay`` parameter is advised in order to throttle sequential calls to the API. By default this is set to ``1`` but can be any integer representing the number of seconds to delay between calls.
|
||||
|
||||
Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34am to the ``/home/blink`` directory with a 2s delay between calls:
|
||||
|
||||
.. code:: python
|
||||
|
||||
blink.download_videos('/home/blink', since='2018/07/04 09:34', delay=2)
|
||||
|
||||
|
||||
.. |Build Status| image:: https://github.com/fronzbot/blinkpy/workflows/build/badge.svg
|
||||
@@ -157,7 +191,5 @@ Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34a
|
||||
:target: https://pypi.python.org/pypi/blinkpy
|
||||
.. |Docs| image:: https://readthedocs.org/projects/blinkpy/badge/?version=latest
|
||||
:target: http://blinkpy.readthedocs.io/en/latest/?badge=latest
|
||||
.. |Donate| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif
|
||||
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UR6Z2B8GXYUCC
|
||||
.. |Codestyle| image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/psf/black
|
||||
|
||||
+14
-6
@@ -1,11 +1,12 @@
|
||||
"""Script to run blinkpy as an app."""
|
||||
from os import environ
|
||||
from datetime import datetime, timedelta
|
||||
from blinkpy import blinkpy
|
||||
from blinkpy.blinkpy import Blink
|
||||
from blinkpy.auth import Auth
|
||||
from blinkpy.helpers.util import json_load
|
||||
|
||||
|
||||
USERNAME = environ.get("USERNAME")
|
||||
PASSWORD = environ.get("PASSWORD")
|
||||
CREDFILE = environ.get("CREDFILE")
|
||||
TIMEDELTA = timedelta(environ.get("TIMEDELTA", 1))
|
||||
|
||||
|
||||
@@ -21,11 +22,18 @@ def download_videos(blink, save_dir="/media"):
|
||||
|
||||
def start():
|
||||
"""Startup blink app."""
|
||||
blink = blinkpy.Blink(username=USERNAME, password=PASSWORD)
|
||||
blink = Blink()
|
||||
blink.auth = Auth(json_load(CREDFILE))
|
||||
blink.start()
|
||||
return blink
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the app."""
|
||||
blink = start()
|
||||
download_videos(blink)
|
||||
blink.save(CREDFILE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
BLINK = start()
|
||||
download_videos(BLINK)
|
||||
main()
|
||||
|
||||
+74
-44
@@ -3,11 +3,11 @@
|
||||
import logging
|
||||
from json import dumps
|
||||
from blinkpy.helpers.util import get_time, Throttle
|
||||
from blinkpy.helpers.constants import DEFAULT_URL
|
||||
from blinkpy.helpers.constants import DEFAULT_URL, TIMEOUT, DEFAULT_USER_AGENT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_THROTTLE_TIME = 2
|
||||
MIN_THROTTLE_TIME = 5
|
||||
|
||||
|
||||
def request_login(
|
||||
@@ -20,21 +20,22 @@ def request_login(
|
||||
:param url: Login url.
|
||||
:login_data: Dictionary containing blink login data.
|
||||
"""
|
||||
headers = {"Host": DEFAULT_URL, "Content-Type": "application/json"}
|
||||
headers = {
|
||||
"Host": DEFAULT_URL,
|
||||
"Content-Type": "application/json",
|
||||
"user-agent": DEFAULT_USER_AGENT,
|
||||
}
|
||||
data = dumps(
|
||||
{
|
||||
"email": login_data["username"],
|
||||
"password": login_data["password"],
|
||||
"notification_key": login_data["notification_key"],
|
||||
"unique_id": login_data["uid"],
|
||||
"app_version": "6.0.7 (520300) #afb0be72a",
|
||||
"device_identifier": login_data["device_id"],
|
||||
"client_name": "Computer",
|
||||
"client_type": "android",
|
||||
"os_version": "5.1.1",
|
||||
"reauth": "true",
|
||||
"reauth": True,
|
||||
}
|
||||
)
|
||||
|
||||
return auth.query(
|
||||
url=url,
|
||||
headers=headers,
|
||||
@@ -47,18 +48,39 @@ def request_login(
|
||||
|
||||
def request_verify(auth, 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
|
||||
)
|
||||
url = f"{blink.urls.base_url}/api/v4/account/{blink.account_id}/client/{blink.client_id}/pin/verify"
|
||||
data = dumps({"pin": verify_key})
|
||||
return auth.query(
|
||||
url=url, headers=auth.header, data=data, json_resp=False, reqtype="post",
|
||||
)
|
||||
|
||||
|
||||
def request_logout(blink):
|
||||
"""Logout of blink servers."""
|
||||
url = f"{blink.urls.base_url}/api/v4/account/{blink.account_id}/client/{blink.client_id}/logout"
|
||||
return http_post(blink, url=url)
|
||||
|
||||
|
||||
def request_networks(blink):
|
||||
"""Request all networks information."""
|
||||
url = "{}/networks".format(blink.urls.base_url)
|
||||
url = f"{blink.urls.base_url}/networks"
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
def request_network_update(blink, network):
|
||||
"""
|
||||
Request network update.
|
||||
|
||||
:param blink: Blink instance.
|
||||
:param network: Sync module network id.
|
||||
"""
|
||||
url = f"{blink.urls.base_url}/network/{network}/update"
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
def request_user(blink):
|
||||
"""Get user information from blink servers."""
|
||||
url = f"{blink.urls.base_url}/user"
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@@ -69,7 +91,7 @@ def request_network_status(blink, network):
|
||||
:param blink: Blink instance.
|
||||
:param network: Sync module network id.
|
||||
"""
|
||||
url = "{}/network/{}".format(blink.urls.base_url, network)
|
||||
url = f"{blink.urls.base_url}/network/{network}"
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@@ -80,7 +102,7 @@ def request_syncmodule(blink, network):
|
||||
:param blink: Blink instance.
|
||||
:param network: Sync module network id.
|
||||
"""
|
||||
url = "{}/network/{}/syncmodules".format(blink.urls.base_url, network)
|
||||
url = f"{blink.urls.base_url}/network/{network}/syncmodules"
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@@ -92,7 +114,7 @@ def request_system_arm(blink, network):
|
||||
:param blink: Blink instance.
|
||||
:param network: Sync module network id.
|
||||
"""
|
||||
url = "{}/network/{}/arm".format(blink.urls.base_url, network)
|
||||
url = f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}/networks/{network}/state/arm"
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
@@ -104,7 +126,7 @@ def request_system_disarm(blink, network):
|
||||
:param blink: Blink instance.
|
||||
:param network: Sync module network id.
|
||||
"""
|
||||
url = "{}/network/{}/disarm".format(blink.urls.base_url, network)
|
||||
url = f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}/networks/{network}/state/disarm"
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
@@ -116,16 +138,14 @@ def request_command_status(blink, network, command_id):
|
||||
:param network: Sync module network id.
|
||||
:param command_id: Command id to check.
|
||||
"""
|
||||
url = "{}/network/{}/command/{}".format(blink.urls.base_url, network, command_id)
|
||||
url = f"{blink.urls.base_url}/network/{network}/command/{command_id}"
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def request_homescreen(blink):
|
||||
"""Request homescreen info."""
|
||||
url = "{}/api/v3/accounts/{}/homescreen".format(
|
||||
blink.urls.base_url, blink.account_id
|
||||
)
|
||||
url = f"{blink.urls.base_url}/api/v3/accounts/{blink.account_id}/homescreen"
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@@ -137,7 +157,7 @@ def request_sync_events(blink, network):
|
||||
:param blink: Blink instance.
|
||||
:param network: Sync module network id.
|
||||
"""
|
||||
url = "{}/events/network/{}".format(blink.urls.base_url, network)
|
||||
url = f"{blink.urls.base_url}/events/network/{network}"
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@@ -150,9 +170,7 @@ def request_new_image(blink, network, camera_id):
|
||||
:param network: Sync module network id.
|
||||
:param camera_id: Camera ID of camera to request new image from.
|
||||
"""
|
||||
url = "{}/network/{}/camera/{}/thumbnail".format(
|
||||
blink.urls.base_url, network, camera_id
|
||||
)
|
||||
url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/thumbnail"
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
@@ -165,14 +183,14 @@ def request_new_video(blink, network, camera_id):
|
||||
:param network: Sync module network id.
|
||||
:param camera_id: Camera ID of camera to request new video from.
|
||||
"""
|
||||
url = "{}/network/{}/camera/{}/clip".format(blink.urls.base_url, network, camera_id)
|
||||
url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/clip"
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def request_video_count(blink):
|
||||
"""Request total video count."""
|
||||
url = "{}/api/v2/videos/count".format(blink.urls.base_url)
|
||||
url = f"{blink.urls.base_url}/api/v2/videos/count"
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@@ -185,9 +203,7 @@ def request_videos(blink, time=None, page=0):
|
||||
:param page: Page number to get videos from.
|
||||
"""
|
||||
timestamp = get_time(time)
|
||||
url = "{}/api/v1/accounts/{}/media/changed?since={}&page={}".format(
|
||||
blink.urls.base_url, blink.account_id, timestamp, page
|
||||
)
|
||||
url = f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}/media/changed?since={timestamp}&page={page}"
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@@ -198,7 +214,7 @@ def request_cameras(blink, network):
|
||||
:param Blink: Blink instance.
|
||||
:param network: Sync module network id.
|
||||
"""
|
||||
url = "{}/network/{}/cameras".format(blink.urls.base_url, network)
|
||||
url = f"{blink.urls.base_url}/network/{network}/cameras"
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@@ -210,12 +226,32 @@ def request_camera_info(blink, network, camera_id):
|
||||
:param network: Sync module network id.
|
||||
:param camera_id: Camera ID of camera to request info from.
|
||||
"""
|
||||
url = "{}/network/{}/camera/{}/config".format(
|
||||
blink.urls.base_url, network, camera_id
|
||||
)
|
||||
url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/config"
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
def request_camera_usage(blink):
|
||||
"""
|
||||
Request camera status.
|
||||
|
||||
:param blink: Blink instance.
|
||||
"""
|
||||
url = f"{blink.urls.base_url}/api/v1/camera/usage"
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
def request_camera_liveview(blink, network, camera_id):
|
||||
"""
|
||||
Request camera liveview.
|
||||
|
||||
:param blink: Blink instance.
|
||||
:param network: Sync module network id.
|
||||
:param camera_id: Camera ID of camera to request liveview from.
|
||||
"""
|
||||
url = f"{blink.urls.base_url}/api/v5/accounts/{blink.account_id}/networks/{network}/cameras/{camera_id}/liveview"
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
def request_camera_sensors(blink, network, camera_id):
|
||||
"""
|
||||
Request camera sensor info for one camera.
|
||||
@@ -224,9 +260,7 @@ def request_camera_sensors(blink, network, camera_id):
|
||||
:param network: Sync module network id.
|
||||
:param camera_id: Camera ID of camera to request sesnor info from.
|
||||
"""
|
||||
url = "{}/network/{}/camera/{}/signals".format(
|
||||
blink.urls.base_url, network, camera_id
|
||||
)
|
||||
url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/signals"
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@@ -239,9 +273,7 @@ def request_motion_detection_enable(blink, network, camera_id):
|
||||
:param network: Sync module network id.
|
||||
:param camera_id: Camera ID of camera to enable.
|
||||
"""
|
||||
url = "{}/network/{}/camera/{}/enable".format(
|
||||
blink.urls.base_url, network, camera_id
|
||||
)
|
||||
url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/enable"
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
@@ -253,13 +285,11 @@ def request_motion_detection_disable(blink, network, camera_id):
|
||||
:param network: Sync module network id.
|
||||
:param camera_id: Camera ID of camera to disable.
|
||||
"""
|
||||
url = "{}/network/{}/camera/{}/disable".format(
|
||||
blink.urls.base_url, network, camera_id
|
||||
)
|
||||
url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/disable"
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
def http_get(blink, url, stream=False, json=True, is_retry=False):
|
||||
def http_get(blink, url, stream=False, json=True, is_retry=False, timeout=TIMEOUT):
|
||||
"""
|
||||
Perform an http get request.
|
||||
|
||||
@@ -279,7 +309,7 @@ def http_get(blink, url, stream=False, json=True, is_retry=False):
|
||||
)
|
||||
|
||||
|
||||
def http_post(blink, url, is_retry=False):
|
||||
def http_post(blink, url, is_retry=False, timeout=TIMEOUT):
|
||||
"""
|
||||
Perform an http post request.
|
||||
|
||||
|
||||
+101
-26
@@ -2,10 +2,16 @@
|
||||
import logging
|
||||
from functools import partial
|
||||
from requests import Request, Session, exceptions
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
from blinkpy import api
|
||||
from blinkpy.helpers import util
|
||||
from blinkpy.helpers.constants import BLINK_URL, LOGIN_ENDPOINT
|
||||
from blinkpy.helpers import errors as ERROR
|
||||
from blinkpy.helpers.constants import (
|
||||
BLINK_URL,
|
||||
DEFAULT_USER_AGENT,
|
||||
LOGIN_ENDPOINT,
|
||||
TIMEOUT,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -33,6 +39,7 @@ class Auth:
|
||||
self.client_id = login_data.get("client_id", None)
|
||||
self.account_id = login_data.get("account_id", None)
|
||||
self.login_response = None
|
||||
self.is_errored = False
|
||||
self.no_prompt = no_prompt
|
||||
self.session = self.create_session()
|
||||
|
||||
@@ -51,12 +58,31 @@ class Auth:
|
||||
"""Return authorization header."""
|
||||
if self.token is None:
|
||||
return None
|
||||
return {"Host": self.host, "TOKEN_AUTH": self.token}
|
||||
return {
|
||||
"TOKEN_AUTH": self.token,
|
||||
"user-agent": DEFAULT_USER_AGENT,
|
||||
"content-type": "application/json",
|
||||
}
|
||||
|
||||
def create_session(self):
|
||||
def create_session(self, opts=None):
|
||||
"""Create a session for blink communication."""
|
||||
if opts is None:
|
||||
opts = {}
|
||||
backoff = opts.get("backoff", 1)
|
||||
retries = opts.get("retries", 3)
|
||||
retry_list = opts.get("retry_list", [429, 500, 502, 503, 504])
|
||||
sess = Session()
|
||||
sess.get = partial(sess.get, timeout=10)
|
||||
assert_status_hook = [
|
||||
lambda response, *args, **kwargs: response.raise_for_status()
|
||||
]
|
||||
sess.hooks["response"] = assert_status_hook
|
||||
retry = Retry(
|
||||
total=retries, backoff_factor=backoff, status_forcelist=retry_list
|
||||
)
|
||||
adapter = HTTPAdapter(max_retries=retry)
|
||||
sess.mount("https://", adapter)
|
||||
sess.mount("http://", adapter)
|
||||
sess.get = partial(sess.get, timeout=TIMEOUT)
|
||||
return sess
|
||||
|
||||
def prepare_request(self, url, headers, data, reqtype):
|
||||
@@ -82,24 +108,37 @@ class Auth:
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
raise LoginError
|
||||
except AttributeError:
|
||||
raise LoginError
|
||||
except AttributeError as error:
|
||||
raise LoginError from error
|
||||
|
||||
def logout(self, blink):
|
||||
"""Log out."""
|
||||
return api.request_logout(blink)
|
||||
|
||||
def refresh_token(self):
|
||||
"""Refresh auth token."""
|
||||
self.is_errored = True
|
||||
try:
|
||||
_LOGGER.info("Token expired, attempting automatic refresh.")
|
||||
self.login_response = self.login()
|
||||
self.region_id = self.login_response["region"]["tier"]
|
||||
self.host = f"{self.region_id}.{BLINK_URL}"
|
||||
self.token = self.login_response["authtoken"]["authtoken"]
|
||||
self.client_id = self.login_response["client"]["id"]
|
||||
self.account_id = self.login_response["account"]["id"]
|
||||
except KeyError:
|
||||
self.extract_login_info()
|
||||
self.is_errored = False
|
||||
except LoginError as error:
|
||||
_LOGGER.error("Login endpoint failed. Try again later.")
|
||||
raise TokenRefreshFailed from error
|
||||
except (TypeError, KeyError) as error:
|
||||
_LOGGER.error("Malformed login response: %s", self.login_response)
|
||||
raise TokenRefreshFailed
|
||||
raise TokenRefreshFailed from error
|
||||
return True
|
||||
|
||||
def extract_login_info(self):
|
||||
"""Extract login info from login response."""
|
||||
self.region_id = self.login_response["account"]["tier"]
|
||||
self.host = f"{self.region_id}.{BLINK_URL}"
|
||||
self.token = self.login_response["auth"]["token"]
|
||||
self.client_id = self.login_response["account"]["client_id"]
|
||||
self.account_id = self.login_response["account"]["account_id"]
|
||||
|
||||
def startup(self):
|
||||
"""Initialize tokens for communication."""
|
||||
self.validate_login()
|
||||
@@ -109,15 +148,21 @@ class Auth:
|
||||
def validate_response(self, response, json_resp):
|
||||
"""Check for valid response."""
|
||||
if not json_resp:
|
||||
self.is_errored = False
|
||||
return response
|
||||
|
||||
json_data = response.json()
|
||||
self.is_errored = True
|
||||
try:
|
||||
if json_data["code"] in ERROR.BLINK_ERRORS:
|
||||
if response.status_code in [101, 401]:
|
||||
raise UnauthorizedError
|
||||
if response.status_code == 404:
|
||||
raise exceptions.ConnectionError
|
||||
json_data = response.json()
|
||||
except KeyError:
|
||||
pass
|
||||
except (AttributeError, ValueError) as error:
|
||||
raise BlinkBadResponse from error
|
||||
|
||||
self.is_errored = False
|
||||
return json_data
|
||||
|
||||
def query(
|
||||
@@ -129,6 +174,7 @@ class Auth:
|
||||
stream=False,
|
||||
json_resp=True,
|
||||
is_retry=False,
|
||||
timeout=TIMEOUT,
|
||||
):
|
||||
"""
|
||||
Perform server requests.
|
||||
@@ -139,29 +185,47 @@ class Auth:
|
||||
:param reqtype: Can be 'get' or 'post' (default: 'get')
|
||||
:param stream: Stream 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 part of a re-auth attempt? True/FALSE
|
||||
"""
|
||||
req = self.prepare_request(url, headers, data, reqtype)
|
||||
try:
|
||||
response = self.session.send(req, stream=stream)
|
||||
response = self.session.send(req, stream=stream, timeout=timeout)
|
||||
return self.validate_response(response, json_resp)
|
||||
|
||||
except (exceptions.ConnectionError, exceptions.Timeout, TokenRefreshFailed):
|
||||
except (exceptions.ConnectionError, exceptions.Timeout):
|
||||
_LOGGER.error(
|
||||
"Connection error. Endpoint %s possibly down or throttled.", url,
|
||||
)
|
||||
except BlinkBadResponse:
|
||||
code = None
|
||||
reason = None
|
||||
try:
|
||||
code = response.status_code
|
||||
reason = response.reason
|
||||
except AttributeError:
|
||||
pass
|
||||
_LOGGER.error(
|
||||
"Expected json response from %s, but received: %s: %s",
|
||||
url,
|
||||
code,
|
||||
reason,
|
||||
)
|
||||
except UnauthorizedError:
|
||||
try:
|
||||
if not is_retry:
|
||||
self.refresh_token()
|
||||
return self.query(
|
||||
url=url,
|
||||
data=data,
|
||||
headers=headers,
|
||||
headers=self.header,
|
||||
reqtype=reqtype,
|
||||
stream=stream,
|
||||
json_resp=json_resp,
|
||||
is_retry=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
except (TokenRefreshFailed, LoginError):
|
||||
_LOGGER.error("Endpoint %s failed. Unable to refresh login tokens", url)
|
||||
_LOGGER.error("Endpoint %s failed", url)
|
||||
_LOGGER.error("Unable to access %s after token refresh.", url)
|
||||
except TokenRefreshFailed:
|
||||
_LOGGER.error("Unable to refresh token.")
|
||||
return None
|
||||
|
||||
def send_auth_key(self, blink, key):
|
||||
@@ -171,6 +235,9 @@ class Auth:
|
||||
try:
|
||||
json_resp = response.json()
|
||||
blink.available = json_resp["valid"]
|
||||
if not json_resp["valid"]:
|
||||
_LOGGER.error("%s", json_resp["message"])
|
||||
return False
|
||||
except (KeyError, TypeError):
|
||||
_LOGGER.error("Did not receive valid response from server.")
|
||||
return False
|
||||
@@ -179,7 +246,7 @@ class Auth:
|
||||
def check_key_required(self):
|
||||
"""Check if 2FA key is required."""
|
||||
try:
|
||||
if self.login_response["client"]["verification_required"]:
|
||||
if self.login_response["account"]["client_verification_required"]:
|
||||
return True
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
@@ -192,3 +259,11 @@ class TokenRefreshFailed(Exception):
|
||||
|
||||
class LoginError(Exception):
|
||||
"""Class to throw failed login exception."""
|
||||
|
||||
|
||||
class BlinkBadResponse(Exception):
|
||||
"""Class to throw bad json response exception."""
|
||||
|
||||
|
||||
class UnauthorizedError(Exception):
|
||||
"""Class to throw an unauthorized access error."""
|
||||
|
||||
+88
-30
@@ -23,17 +23,17 @@ from dateutil.parser import parse
|
||||
from slugify import slugify
|
||||
|
||||
from blinkpy import api
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.sync_module import BlinkSyncModule, BlinkOwl
|
||||
from blinkpy.helpers import util
|
||||
from blinkpy.helpers.constants import (
|
||||
DEFAULT_MOTION_INTERVAL,
|
||||
DEFAULT_REFRESH,
|
||||
MIN_THROTTLE_TIME,
|
||||
TIMEOUT_MEDIA,
|
||||
)
|
||||
from blinkpy.helpers.constants import __version__
|
||||
from blinkpy.auth import Auth, TokenRefreshFailed, LoginError
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -41,7 +41,10 @@ class Blink:
|
||||
"""Class to initialize communication."""
|
||||
|
||||
def __init__(
|
||||
self, refresh_rate=DEFAULT_REFRESH, motion_interval=DEFAULT_MOTION_INTERVAL,
|
||||
self,
|
||||
refresh_rate=DEFAULT_REFRESH,
|
||||
motion_interval=DEFAULT_MOTION_INTERVAL,
|
||||
no_owls=False,
|
||||
):
|
||||
"""
|
||||
Initialize Blink system.
|
||||
@@ -52,6 +55,7 @@ class Blink:
|
||||
Defaults to last refresh time.
|
||||
Useful for preventing motion_detected property
|
||||
from de-asserting too quickly.
|
||||
:param no_owls: Disable searching for owl entries (blink mini cameras only known entity). Prevents an uneccessary API call if you don't have these in your network.
|
||||
"""
|
||||
self.auth = Auth()
|
||||
self.account_id = None
|
||||
@@ -68,23 +72,26 @@ class Blink:
|
||||
self.version = __version__
|
||||
self.available = False
|
||||
self.key_required = False
|
||||
self.homescreen = {}
|
||||
self.no_owls = no_owls
|
||||
|
||||
@util.Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def refresh(self, force=False):
|
||||
def refresh(self, force=False, force_cache=False):
|
||||
"""
|
||||
Perform a system refresh.
|
||||
|
||||
:param force: Force an update of the camera data
|
||||
:param force: Used to override throttle, resets refresh
|
||||
:param force_cache: Used to force update without overriding throttle
|
||||
"""
|
||||
if self.check_if_ok_to_update() or force:
|
||||
if self.check_if_ok_to_update() or force or force_cache:
|
||||
if not self.available:
|
||||
self.auth.refresh_token()
|
||||
self.setup_post_verify()
|
||||
|
||||
self.get_homescreen()
|
||||
for sync_name, sync_module in self.sync.items():
|
||||
_LOGGER.debug("Attempting refresh of sync %s", sync_name)
|
||||
sync_module.refresh(force_cache=force)
|
||||
if not force:
|
||||
sync_module.refresh(force_cache=(force or force_cache))
|
||||
if not force_cache:
|
||||
# Prevents rapid clearing of motion detect property
|
||||
self.last_refresh = int(time.time())
|
||||
return True
|
||||
@@ -96,6 +103,7 @@ class Blink:
|
||||
self.auth.startup()
|
||||
self.setup_login_ids()
|
||||
self.setup_urls()
|
||||
self.get_homescreen()
|
||||
except (LoginError, TokenRefreshFailed, BlinkSetupError):
|
||||
_LOGGER.error("Cannot setup Blink platform.")
|
||||
self.available = False
|
||||
@@ -128,7 +136,8 @@ class Blink:
|
||||
for name, network_id in networks.items():
|
||||
sync_cameras = cameras.get(network_id, {})
|
||||
self.setup_sync_module(name, network_id, sync_cameras)
|
||||
self.cameras = self.merge_cameras()
|
||||
|
||||
self.cameras = self.merge_cameras()
|
||||
|
||||
self.available = True
|
||||
self.key_required = False
|
||||
@@ -139,20 +148,57 @@ class Blink:
|
||||
self.sync[name] = BlinkSyncModule(self, name, network_id, cameras)
|
||||
self.sync[name].start()
|
||||
|
||||
def get_homescreen(self):
|
||||
"""Get homecreen information."""
|
||||
if self.no_owls:
|
||||
_LOGGER.debug("Skipping owl extraction.")
|
||||
self.homescreen = {}
|
||||
return
|
||||
self.homescreen = api.request_homescreen(self)
|
||||
|
||||
def setup_owls(self):
|
||||
"""Check for mini cameras."""
|
||||
network_list = []
|
||||
camera_list = []
|
||||
try:
|
||||
for owl in self.homescreen["owls"]:
|
||||
name = owl["name"]
|
||||
network_id = str(owl["network_id"])
|
||||
if network_id in self.network_ids:
|
||||
camera_list.append(
|
||||
{network_id: {"name": name, "id": network_id, "type": "mini"}}
|
||||
)
|
||||
continue
|
||||
if owl["onboarded"]:
|
||||
network_list.append(str(network_id))
|
||||
self.sync[name] = BlinkOwl(self, name, network_id, owl)
|
||||
self.sync[name].start()
|
||||
except KeyError:
|
||||
# No sync-less devices found
|
||||
pass
|
||||
|
||||
self.network_ids.extend(network_list)
|
||||
return camera_list
|
||||
|
||||
def setup_camera_list(self):
|
||||
"""Create camera list for onboarded networks."""
|
||||
all_cameras = {}
|
||||
response = api.request_homescreen(self)
|
||||
response = api.request_camera_usage(self)
|
||||
try:
|
||||
for camera in response["cameras"]:
|
||||
camera_network = str(camera["network_id"])
|
||||
for network in response["networks"]:
|
||||
camera_network = str(network["network_id"])
|
||||
if camera_network not in all_cameras:
|
||||
all_cameras[camera_network] = []
|
||||
all_cameras[camera_network].append(
|
||||
{"name": camera["name"], "id": camera["id"]}
|
||||
)
|
||||
for camera in network["cameras"]:
|
||||
all_cameras[camera_network].append(
|
||||
{"name": camera["name"], "id": camera["id"]}
|
||||
)
|
||||
mini_cameras = self.setup_owls()
|
||||
for camera in mini_cameras:
|
||||
for network, camera_info in camera.items():
|
||||
all_cameras[network].append(camera_info)
|
||||
return all_cameras
|
||||
except KeyError:
|
||||
except (KeyError, TypeError):
|
||||
_LOGGER.error("Unable to retrieve cameras from response %s", response)
|
||||
raise BlinkSetupError
|
||||
|
||||
@@ -176,7 +222,7 @@ class Blink:
|
||||
response = api.request_networks(self)
|
||||
try:
|
||||
self.networks = response["summary"]
|
||||
except KeyError:
|
||||
except (KeyError, TypeError):
|
||||
raise BlinkSetupError
|
||||
|
||||
def setup_network_ids(self):
|
||||
@@ -218,17 +264,20 @@ class Blink:
|
||||
"""Save login data to file."""
|
||||
util.json_save(self.auth.login_attributes, file_name)
|
||||
|
||||
def download_videos(self, path, since=None, camera="all", stop=10, debug=False):
|
||||
def download_videos(
|
||||
self, path, since=None, camera="all", stop=10, delay=1, debug=False
|
||||
):
|
||||
"""
|
||||
Download all videos from server since specified time.
|
||||
|
||||
:param path: Path to write files. /path/<cameraname>_<recorddate>.mp4
|
||||
:param since: Date and time to get videos from.
|
||||
Ex: "2018/07/28 12:33:00" to retrieve videos since
|
||||
July 28th 2018 at 12:33:00
|
||||
July 28th 2018 at 12:33:00
|
||||
:param camera: Camera name to retrieve. Defaults to "all".
|
||||
Use a list for multiple cameras.
|
||||
:param stop: Page to stop on (~25 items per page. Default page 10).
|
||||
:param delay: Number of seconds to wait in between subsequent video downloads.
|
||||
:param debug: Set to TRUE to prevent downloading of items.
|
||||
Instead of downloading, entries will be printed to log.
|
||||
"""
|
||||
@@ -250,14 +299,14 @@ class Blink:
|
||||
try:
|
||||
result = response["media"]
|
||||
if not result:
|
||||
raise IndexError
|
||||
except (KeyError, IndexError):
|
||||
raise KeyError
|
||||
except (KeyError, TypeError):
|
||||
_LOGGER.info("No videos found on page %s. Exiting.", page)
|
||||
break
|
||||
|
||||
self._parse_downloaded_items(result, camera, path, debug)
|
||||
self._parse_downloaded_items(result, camera, path, delay, debug)
|
||||
|
||||
def _parse_downloaded_items(self, result, camera, path, debug):
|
||||
def _parse_downloaded_items(self, result, camera, path, delay, debug):
|
||||
"""Parse downloaded videos."""
|
||||
for item in result:
|
||||
try:
|
||||
@@ -277,9 +326,9 @@ class Blink:
|
||||
_LOGGER.debug("%s: %s is marked as deleted.", camera_name, address)
|
||||
continue
|
||||
|
||||
clip_address = "{}{}".format(self.urls.base_url, address)
|
||||
filename = "{}-{}".format(camera_name, created_at)
|
||||
filename = "{}.mp4".format(slugify(filename))
|
||||
clip_address = f"{self.urls.base_url}{address}"
|
||||
filename = f"{camera_name}-{created_at}"
|
||||
filename = f"{slugify(filename)}.mp4"
|
||||
filename = os.path.join(path, filename)
|
||||
|
||||
if not debug:
|
||||
@@ -287,17 +336,26 @@ class Blink:
|
||||
_LOGGER.info("%s already exists, skipping...", filename)
|
||||
continue
|
||||
|
||||
response = api.http_get(self, url=clip_address, stream=True, json=False)
|
||||
response = api.http_get(
|
||||
self,
|
||||
url=clip_address,
|
||||
stream=True,
|
||||
json=False,
|
||||
timeout=TIMEOUT_MEDIA,
|
||||
)
|
||||
with open(filename, "wb") as vidfile:
|
||||
copyfileobj(response.raw, vidfile)
|
||||
|
||||
_LOGGER.info("Downloaded video to %s", filename)
|
||||
else:
|
||||
print(
|
||||
("Camera: {}, Timestamp: {}, " "Address: {}, Filename: {}").format(
|
||||
camera_name, created_at, address, filename
|
||||
(
|
||||
f"Camera: {camera_name}, Timestamp: {created_at}, "
|
||||
"Address: {address}, Filename: {filename}"
|
||||
)
|
||||
)
|
||||
if delay > 0:
|
||||
time.sleep(delay)
|
||||
|
||||
|
||||
class BlinkSetupError(Exception):
|
||||
|
||||
+117
-31
@@ -3,6 +3,7 @@
|
||||
from shutil import copyfileobj
|
||||
import logging
|
||||
from blinkpy import api
|
||||
from blinkpy.helpers.constants import TIMEOUT_MEDIA
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,6 +30,7 @@ class BlinkCamera:
|
||||
self.last_record = None
|
||||
self._cached_image = None
|
||||
self._cached_video = None
|
||||
self.camera_type = ""
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
@@ -61,7 +63,10 @@ class BlinkCamera:
|
||||
@property
|
||||
def temperature_c(self):
|
||||
"""Return temperature in celcius."""
|
||||
return round((self.temperature - 32) / 9.0 * 5.0, 1)
|
||||
try:
|
||||
return round((self.temperature - 32) / 9.0 * 5.0, 1)
|
||||
except TypeError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def image_from_cache(self):
|
||||
@@ -77,12 +82,44 @@ class BlinkCamera:
|
||||
return self._cached_video
|
||||
return None
|
||||
|
||||
@property
|
||||
def arm(self):
|
||||
"""Return arm status of camera."""
|
||||
return self.motion_enabled
|
||||
|
||||
@arm.setter
|
||||
def arm(self, value):
|
||||
"""Set camera arm status."""
|
||||
if value:
|
||||
return api.request_motion_detection_enable(
|
||||
self.sync.blink, self.network_id, self.camera_id
|
||||
)
|
||||
return api.request_motion_detection_disable(
|
||||
self.sync.blink, self.network_id, self.camera_id
|
||||
)
|
||||
|
||||
def record(self):
|
||||
"""Initiate clip recording."""
|
||||
return api.request_new_video(self.sync.blink, self.network_id, self.camera_id)
|
||||
|
||||
def get_media(self, media_type="image"):
|
||||
"""Download media (image or video)."""
|
||||
url = self.thumbnail
|
||||
if media_type.lower() == "video":
|
||||
url = self.clip
|
||||
return api.http_get(
|
||||
self.sync.blink, url=url, stream=True, json=False, timeout=TIMEOUT_MEDIA,
|
||||
)
|
||||
|
||||
def snap_picture(self):
|
||||
"""Take a picture with camera to create a new thumbnail."""
|
||||
return api.request_new_image(self.sync.blink, self.network_id, self.camera_id)
|
||||
|
||||
def set_motion_detect(self, enable):
|
||||
"""Set motion detection."""
|
||||
_LOGGER.warning(
|
||||
"Method is deprecated as of v0.16.0 and will be removed in a future version. Please use the BlinkCamera.arm property instead."
|
||||
)
|
||||
if enable:
|
||||
return api.request_motion_detection_enable(
|
||||
self.sync.blink, self.network_id, self.camera_id
|
||||
@@ -93,18 +130,24 @@ class BlinkCamera:
|
||||
|
||||
def update(self, config, force_cache=False, **kwargs):
|
||||
"""Update camera info."""
|
||||
# force = kwargs.pop('force', False)
|
||||
self.name = config["name"]
|
||||
self.camera_id = str(config["id"])
|
||||
self.network_id = str(config["network_id"])
|
||||
self.serial = config["serial"]
|
||||
self.motion_enabled = config["enabled"]
|
||||
self.battery_voltage = config["battery_voltage"]
|
||||
self.battery_state = config["battery_state"]
|
||||
self.temperature = config["temperature"]
|
||||
self.wifi_strength = config["wifi_strength"]
|
||||
self.extract_config_info(config)
|
||||
self.get_sensor_info()
|
||||
self.update_images(config, force_cache=force_cache)
|
||||
|
||||
# Retrieve calibrated temperature from special endpoint
|
||||
def extract_config_info(self, config):
|
||||
"""Extract info from config."""
|
||||
self.name = config.get("name", "unknown")
|
||||
self.camera_id = str(config.get("id", "unknown"))
|
||||
self.network_id = str(config.get("network_id", "unknown"))
|
||||
self.serial = config.get("serial", None)
|
||||
self.motion_enabled = config.get("enabled", "unknown")
|
||||
self.battery_voltage = config.get("battery_voltage", None)
|
||||
self.battery_state = config.get("battery_state", None)
|
||||
self.temperature = config.get("temperature", None)
|
||||
self.wifi_strength = config.get("wifi_strength", None)
|
||||
|
||||
def get_sensor_info(self):
|
||||
"""Retrieve calibrated temperatue from special endpoint."""
|
||||
resp = api.request_camera_sensors(
|
||||
self.sync.blink, self.network_id, self.camera_id
|
||||
)
|
||||
@@ -114,20 +157,17 @@ class BlinkCamera:
|
||||
self.temperature_calibrated = self.temperature
|
||||
_LOGGER.warning("Could not retrieve calibrated temperature.")
|
||||
|
||||
# Check if thumbnail exists in config, if not try to
|
||||
# get it from the homescreen info in the sync module
|
||||
# otherwise set it to None and log an error
|
||||
def update_images(self, config, force_cache=False):
|
||||
"""Update images for camera."""
|
||||
new_thumbnail = None
|
||||
thumb_addr = None
|
||||
if config["thumbnail"]:
|
||||
if config.get("thumbnail", False):
|
||||
thumb_addr = config["thumbnail"]
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Could not find thumbnail for camera %s", self.name, exc_info=True
|
||||
)
|
||||
_LOGGER.warning("Could not find thumbnail for camera %s", self.name)
|
||||
|
||||
if thumb_addr is not None:
|
||||
new_thumbnail = "{}{}.jpg".format(self.sync.urls.base_url, thumb_addr)
|
||||
new_thumbnail = f"{self.sync.urls.base_url}{thumb_addr}.jpg"
|
||||
|
||||
try:
|
||||
self.motion_detected = self.sync.motion[self.name]
|
||||
@@ -135,10 +175,12 @@ class BlinkCamera:
|
||||
self.motion_detected = False
|
||||
|
||||
clip_addr = None
|
||||
if self.name in self.sync.last_record:
|
||||
try:
|
||||
clip_addr = self.sync.last_record[self.name]["clip"]
|
||||
self.last_record = self.sync.last_record[self.name]["time"]
|
||||
self.clip = "{}{}".format(self.sync.urls.base_url, clip_addr)
|
||||
self.clip = f"{self.sync.urls.base_url}{clip_addr}"
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# If the thumbnail or clip have changed, update the cache
|
||||
update_cached_image = False
|
||||
@@ -151,13 +193,17 @@ class BlinkCamera:
|
||||
update_cached_video = True
|
||||
|
||||
if new_thumbnail is not None and (update_cached_image or force_cache):
|
||||
self._cached_image = api.http_get(
|
||||
self.sync.blink, url=self.thumbnail, stream=True, json=False
|
||||
)
|
||||
self._cached_image = self.get_media()
|
||||
|
||||
if clip_addr is not None and (update_cached_video or force_cache):
|
||||
self._cached_video = api.http_get(
|
||||
self.sync.blink, url=self.clip, stream=True, json=False
|
||||
)
|
||||
self._cached_video = self.get_media(media_type="video")
|
||||
|
||||
def get_liveview(self):
|
||||
"""Get livewview rtsps link."""
|
||||
response = api.request_camera_liveview(
|
||||
self.sync.blink, self.sync.network_id, self.camera_id
|
||||
)
|
||||
return response["server"]
|
||||
|
||||
def image_to_file(self, path):
|
||||
"""
|
||||
@@ -166,7 +212,7 @@ class BlinkCamera:
|
||||
:param path: Path to write file
|
||||
"""
|
||||
_LOGGER.debug("Writing image from %s to %s", self.name, path)
|
||||
response = self._cached_image
|
||||
response = self.get_media()
|
||||
if response.status_code == 200:
|
||||
with open(path, "wb") as imgfile:
|
||||
copyfileobj(response.raw, imgfile)
|
||||
@@ -176,14 +222,54 @@ class BlinkCamera:
|
||||
)
|
||||
|
||||
def video_to_file(self, path):
|
||||
"""Write video to file.
|
||||
"""
|
||||
Write video to file.
|
||||
|
||||
:param path: Path to write file
|
||||
"""
|
||||
_LOGGER.debug("Writing video from %s to %s", self.name, path)
|
||||
response = self._cached_video
|
||||
response = self.get_media(media_type="video")
|
||||
if response is None:
|
||||
_LOGGER.error("No saved video exist for %s.", self.name)
|
||||
return
|
||||
with open(path, "wb") as vidfile:
|
||||
copyfileobj(response.raw, vidfile)
|
||||
|
||||
|
||||
class BlinkCameraMini(BlinkCamera):
|
||||
"""Define a class for a Blink Mini camera."""
|
||||
|
||||
def __init__(self, sync):
|
||||
"""Initialize a Blink Mini cameras."""
|
||||
super().__init__(sync)
|
||||
self.camera_type = "mini"
|
||||
|
||||
@property
|
||||
def arm(self):
|
||||
"""Return camera arm status."""
|
||||
return self.sync.arm
|
||||
|
||||
@arm.setter
|
||||
def arm(self, value):
|
||||
"""Set camera arm status."""
|
||||
_LOGGER.warning(
|
||||
"Individual camera motion detection enable/disable for Blink Mini cameras is unsupported at this time."
|
||||
)
|
||||
|
||||
def snap_picture(self):
|
||||
"""Snap picture for a blink mini camera."""
|
||||
url = f"{self.sync.urls.base_url}/api/v1/accounts/{self.sync.blink.account_id}/networks/{self.network_id}/owls/{self.camera_id}/thumbnail"
|
||||
return api.http_post(self.sync.blink, url)
|
||||
|
||||
def get_sensor_info(self):
|
||||
"""Get sensor info for blink mini camera."""
|
||||
|
||||
def get_liveview(self):
|
||||
"""Get liveview link."""
|
||||
url = f"{self.sync.urls.base_url}/api/v1/accounts/{self.sync.blink.account_id}/networks/{self.network_id}/owls/{self.camera_id}/liveview"
|
||||
response = api.http_post(self.sync.blink, url)
|
||||
server = response["server"]
|
||||
server_split = server.split(":")
|
||||
server_split[0] = "rtsps:"
|
||||
link = "".join(server_split)
|
||||
return link
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import os
|
||||
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 16
|
||||
PATCH_VERSION = "0-rc0"
|
||||
MINOR_VERSION = 18
|
||||
PATCH_VERSION = "0.dev0"
|
||||
|
||||
__version__ = "{}.{}.{}".format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
|
||||
__version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}"
|
||||
|
||||
REQUIRED_PYTHON_VER = (3, 6, 0)
|
||||
|
||||
@@ -14,7 +14,7 @@ PROJECT_NAME = "blinkpy"
|
||||
PROJECT_PACKAGE_NAME = "blinkpy"
|
||||
PROJECT_LICENSE = "MIT"
|
||||
PROJECT_AUTHOR = "Kevin Fronczak"
|
||||
PROJECT_COPYRIGHT = " 2017, {}".format(PROJECT_AUTHOR)
|
||||
PROJECT_COPYRIGHT = f" 2017, {PROJECT_AUTHOR}"
|
||||
PROJECT_URL = "https://github.com/fronzbot/blinkpy"
|
||||
PROJECT_EMAIL = "kfronczak@gmail.com"
|
||||
PROJECT_DESCRIPTION = "A Blink camera Python library " "running on Python 3."
|
||||
@@ -34,25 +34,22 @@ PROJECT_CLASSIFIERS = [
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Topic :: Home Automation",
|
||||
]
|
||||
|
||||
PROJECT_GITHUB_USERNAME = "fronzbot"
|
||||
PROJECT_GITHUB_REPOSITORY = "blinkpy"
|
||||
|
||||
PYPI_URL = "https://pypi.python.org/pypi/{}".format(PROJECT_PACKAGE_NAME)
|
||||
PYPI_URL = f"https://pypi.python.org/pypi/{PROJECT_PACKAGE_NAME}"
|
||||
|
||||
"""
|
||||
URLS
|
||||
"""
|
||||
BLINK_URL = "immedia-semi.com"
|
||||
DEFAULT_URL = "{}.{}".format("rest-prod", BLINK_URL)
|
||||
BASE_URL = "https://{}".format(DEFAULT_URL)
|
||||
LOGIN_ENDPOINT = "{}/api/v4/account/login".format(BASE_URL)
|
||||
LOGIN_URLS = [
|
||||
"{}/api/v4/account/login".format(BASE_URL),
|
||||
"{}/api/v3/login".format(BASE_URL),
|
||||
]
|
||||
DEFAULT_URL = f"rest-prod.{BLINK_URL}"
|
||||
BASE_URL = f"https://{DEFAULT_URL}"
|
||||
LOGIN_ENDPOINT = f"{BASE_URL}/api/v5/account/login"
|
||||
|
||||
"""
|
||||
Dictionaries
|
||||
@@ -62,6 +59,7 @@ ONLINE = {"online": True, "offline": False}
|
||||
"""
|
||||
OTHER
|
||||
"""
|
||||
DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"
|
||||
DEVICE_ID = "Blinkpy"
|
||||
TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
|
||||
DEFAULT_MOTION_INTERVAL = 1
|
||||
@@ -69,3 +67,5 @@ DEFAULT_REFRESH = 30
|
||||
MIN_THROTTLE_TIME = 2
|
||||
SIZE_NOTIFICATION_KEY = 152
|
||||
SIZE_UID = 16
|
||||
TIMEOUT = 10
|
||||
TIMEOUT_MEDIA = 90
|
||||
|
||||
@@ -12,4 +12,4 @@ AUTH_TOKEN = (
|
||||
)
|
||||
REQUEST = (4, "Cannot perform request (get/post type incorrect)")
|
||||
|
||||
BLINK_ERRORS = [101, 400, 404]
|
||||
BLINK_ERRORS = [400, 404]
|
||||
|
||||
@@ -33,10 +33,13 @@ def json_save(data, file_name):
|
||||
json.dump(data, json_file, indent=4)
|
||||
|
||||
|
||||
def gen_uid(size):
|
||||
def gen_uid(size, uid_format=False):
|
||||
"""Create a random sring."""
|
||||
full_token = secrets.token_hex(size)
|
||||
return full_token[0:size]
|
||||
if uid_format:
|
||||
token = f"BlinkCamera_{secrets.token_hex(4)}-{secrets.token_hex(2)}-{secrets.token_hex(2)}-{secrets.token_hex(2)}-{secrets.token_hex(6)}"
|
||||
else:
|
||||
token = secrets.token_hex(size)
|
||||
return token
|
||||
|
||||
|
||||
def time_to_seconds(timestamp):
|
||||
@@ -79,10 +82,7 @@ def prompt_login_data(data):
|
||||
|
||||
def validate_login_data(data):
|
||||
"""Check for missing keys."""
|
||||
data["uid"] = data.get("uid", gen_uid(const.SIZE_UID))
|
||||
data["notification_key"] = data.get(
|
||||
"notification_key", gen_uid(const.SIZE_NOTIFICATION_KEY)
|
||||
)
|
||||
data["uid"] = data.get("uid", gen_uid(const.SIZE_UID, uid_format=True))
|
||||
data["device_id"] = data.get("device_id", const.DEVICE_ID)
|
||||
|
||||
return data
|
||||
|
||||
+123
-37
@@ -4,7 +4,7 @@ import logging
|
||||
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from blinkpy import api
|
||||
from blinkpy.camera import BlinkCamera
|
||||
from blinkpy.camera import BlinkCamera, BlinkCameraMini
|
||||
from blinkpy.helpers.util import time_to_seconds
|
||||
from blinkpy.helpers.constants import ONLINE
|
||||
|
||||
@@ -77,24 +77,15 @@ class BlinkSyncModule:
|
||||
|
||||
@arm.setter
|
||||
def arm(self, value):
|
||||
"""Arm or disarm system."""
|
||||
"""Arm or disarm camera."""
|
||||
if value:
|
||||
return api.request_system_arm(self.blink, self.network_id)
|
||||
|
||||
return api.request_system_disarm(self.blink, self.network_id)
|
||||
|
||||
def start(self):
|
||||
"""Initialize the system."""
|
||||
response = api.request_syncmodule(self.blink, self.network_id)
|
||||
try:
|
||||
self.summary = response["syncmodule"]
|
||||
self.network_id = self.summary["network_id"]
|
||||
except (TypeError, KeyError):
|
||||
_LOGGER.error(
|
||||
("Could not retrieve sync module information " "with response: %s"),
|
||||
response,
|
||||
exc_info=True,
|
||||
)
|
||||
response = self.sync_initialize()
|
||||
if not response:
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -102,32 +93,62 @@ class BlinkSyncModule:
|
||||
self.serial = self.summary["serial"]
|
||||
self.status = self.summary["status"]
|
||||
except KeyError:
|
||||
_LOGGER.error(
|
||||
"Could not extract some sync module info: %s", response, exc_info=True
|
||||
)
|
||||
_LOGGER.error("Could not extract some sync module info: %s", response)
|
||||
|
||||
is_ok = self.get_network_info()
|
||||
self.check_new_videos()
|
||||
|
||||
if not is_ok or not self.update_cameras():
|
||||
return False
|
||||
self.available = True
|
||||
return True
|
||||
|
||||
def sync_initialize(self):
|
||||
"""Initialize a sync module."""
|
||||
response = api.request_syncmodule(self.blink, self.network_id)
|
||||
try:
|
||||
self.summary = response["syncmodule"]
|
||||
self.network_id = self.summary["network_id"]
|
||||
except (TypeError, KeyError):
|
||||
_LOGGER.error(
|
||||
"Could not retrieve sync module information with response: %s", response
|
||||
)
|
||||
return False
|
||||
return response
|
||||
|
||||
def update_cameras(self, camera_type=BlinkCamera):
|
||||
"""Update cameras from server."""
|
||||
try:
|
||||
for camera_config in self.camera_list:
|
||||
if "name" not in camera_config:
|
||||
break
|
||||
blink_camera_type = camera_config.get("type", "")
|
||||
name = camera_config["name"]
|
||||
self.cameras[name] = BlinkCamera(self)
|
||||
self.motion[name] = False
|
||||
camera_info = self.get_camera_info(camera_config["id"])
|
||||
owl_info = self.get_owl_info(name)
|
||||
if blink_camera_type == "mini":
|
||||
camera_type = BlinkCameraMini
|
||||
self.cameras[name] = camera_type(self)
|
||||
camera_info = self.get_camera_info(
|
||||
camera_config["id"], owl_info=owl_info
|
||||
)
|
||||
self.cameras[name].update(camera_info, force_cache=True, force=True)
|
||||
except KeyError:
|
||||
_LOGGER.error(
|
||||
"Could not create cameras instances for %s", self.name, exc_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
if not is_ok:
|
||||
except KeyError:
|
||||
_LOGGER.error("Could not create camera instances for %s", self.name)
|
||||
return False
|
||||
self.available = True
|
||||
return True
|
||||
|
||||
def get_owl_info(self, name):
|
||||
"""Extract owl information."""
|
||||
try:
|
||||
for owl in self.blink.homescreen["owls"]:
|
||||
if owl["name"] == name:
|
||||
return owl
|
||||
except (TypeError, KeyError):
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_events(self, **kwargs):
|
||||
"""Retrieve events from server."""
|
||||
force = kwargs.pop("force", False)
|
||||
@@ -135,28 +156,28 @@ class BlinkSyncModule:
|
||||
try:
|
||||
return response["event"]
|
||||
except (TypeError, KeyError):
|
||||
_LOGGER.error("Could not extract events: %s", response, exc_info=True)
|
||||
_LOGGER.error("Could not extract events: %s", response)
|
||||
return False
|
||||
|
||||
def get_camera_info(self, camera_id):
|
||||
def get_camera_info(self, camera_id, **kwargs):
|
||||
"""Retrieve camera information."""
|
||||
owl = kwargs.get("owl_info", None)
|
||||
if owl is not None:
|
||||
return owl
|
||||
response = api.request_camera_info(self.blink, self.network_id, camera_id)
|
||||
try:
|
||||
return response["camera"][0]
|
||||
except (TypeError, KeyError):
|
||||
_LOGGER.error("Could not extract camera info: %s", response, exc_info=True)
|
||||
return []
|
||||
_LOGGER.error("Could not extract camera info: %s", response)
|
||||
return {}
|
||||
|
||||
def get_network_info(self):
|
||||
"""Retrieve network status."""
|
||||
is_errored = False
|
||||
self.network_info = api.request_network_status(self.blink, self.network_id)
|
||||
self.network_info = api.request_network_update(self.blink, self.network_id)
|
||||
try:
|
||||
is_errored = self.network_info["network"]["sync_module_error"]
|
||||
except KeyError:
|
||||
is_errored = True
|
||||
|
||||
if is_errored:
|
||||
if self.network_info["network"]["sync_module_error"]:
|
||||
raise KeyError
|
||||
except (TypeError, KeyError):
|
||||
self.available = False
|
||||
return False
|
||||
return True
|
||||
@@ -168,7 +189,9 @@ class BlinkSyncModule:
|
||||
self.check_new_videos()
|
||||
for camera_name in self.cameras.keys():
|
||||
camera_id = self.cameras[camera_name].camera_id
|
||||
camera_info = self.get_camera_info(camera_id)
|
||||
camera_info = self.get_camera_info(
|
||||
camera_id, owl_info=self.get_owl_info(camera_name)
|
||||
)
|
||||
self.cameras[camera_name].update(camera_info, force_cache=force_cache)
|
||||
self.available = True
|
||||
|
||||
@@ -208,3 +231,66 @@ class BlinkSyncModule:
|
||||
def check_new_video_time(self, timestamp):
|
||||
"""Check if video has timestamp since last refresh."""
|
||||
return time_to_seconds(timestamp) > self.blink.last_refresh
|
||||
|
||||
|
||||
class BlinkOwl(BlinkSyncModule):
|
||||
"""Representation of a sync-less device."""
|
||||
|
||||
def __init__(self, blink, name, network_id, response):
|
||||
"""Initialize a sync-less object."""
|
||||
cameras = [{"name": name, "id": response["id"]}]
|
||||
super().__init__(blink, name, network_id, cameras)
|
||||
self.sync_id = response["id"]
|
||||
self.serial = response["serial"]
|
||||
self.status = response["enabled"]
|
||||
if not self.serial:
|
||||
self.serial = f"{network_id}-{self.sync_id}"
|
||||
|
||||
def sync_initialize(self):
|
||||
"""Initialize a sync-less module."""
|
||||
self.summary = {
|
||||
"id": self.sync_id,
|
||||
"name": self.name,
|
||||
"serial": self.serial,
|
||||
"status": self.status,
|
||||
"onboarded": True,
|
||||
"account_id": self.blink.account_id,
|
||||
"network_id": self.network_id,
|
||||
}
|
||||
return self.summary
|
||||
|
||||
def update_cameras(self, camera_type=BlinkCameraMini):
|
||||
"""Update sync-less cameras."""
|
||||
return super().update_cameras(camera_type=BlinkCameraMini)
|
||||
|
||||
def get_camera_info(self, camera_id, **kwargs):
|
||||
"""Retrieve camera information."""
|
||||
try:
|
||||
for owl in self.blink.homescreen["owls"]:
|
||||
if owl["name"] == self.name:
|
||||
self.status = owl["enabled"]
|
||||
return owl
|
||||
except (TypeError, KeyError):
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_network_info(self):
|
||||
"""Get network info for sync-less module."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def network_info(self):
|
||||
"""Format owl response to resemble sync module."""
|
||||
return {
|
||||
"network": {
|
||||
"id": self.network_id,
|
||||
"name": self.name,
|
||||
"armed": self.status,
|
||||
"sync_module_error": False,
|
||||
"account_id": self.blink.account_id,
|
||||
}
|
||||
}
|
||||
|
||||
@network_info.setter
|
||||
def network_info(self, value):
|
||||
"""Set network_info property."""
|
||||
|
||||
+5
-3
@@ -4,10 +4,12 @@ coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 80%
|
||||
threshold: 2%
|
||||
patch:
|
||||
default:
|
||||
target: 60%
|
||||
threshold: 5%
|
||||
|
||||
comment: true
|
||||
require_ci_to_pass: yes
|
||||
range: 65..90
|
||||
round: down
|
||||
precision: 1
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Login testing script."""
|
||||
import sys
|
||||
import os
|
||||
from blinkpy.blinkpy import Blink
|
||||
from blinkpy.auth import Auth
|
||||
from blinkpy.helpers.util import json_load, json_save
|
||||
|
||||
save_session = False
|
||||
print("")
|
||||
print("Blink Login Debug Script ...")
|
||||
print(" ... Loading previous session information.")
|
||||
cwd = os.getcwd()
|
||||
print(f" ... Looking in {cwd}.")
|
||||
session_path = os.path.join(cwd, ".session_debug")
|
||||
session = json_load(session_path)
|
||||
|
||||
try:
|
||||
auth_file = session["file"]
|
||||
except (TypeError, KeyError):
|
||||
print(" ... Please input location of auth file")
|
||||
auth_file = input(" Must contain username and password: ")
|
||||
save_session = True
|
||||
|
||||
data = json_load(auth_file)
|
||||
if data is None:
|
||||
print(f" ... Please fix file contents of {auth_file}.")
|
||||
print(" ... Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
username = data["username"]
|
||||
password = data["password"]
|
||||
except KeyError:
|
||||
print(f" ... File contents of {auth_file} incorrect.")
|
||||
print(" ... Require username and password at minimum.")
|
||||
print(" ... Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
if save_session:
|
||||
print(f" ... Saving session file to {session_path}.")
|
||||
json_save({"file": auth_file}, session_path)
|
||||
|
||||
blink = Blink()
|
||||
auth = Auth(data)
|
||||
blink.auth = auth
|
||||
|
||||
print(" ... Starting Blink.")
|
||||
print("")
|
||||
blink.start()
|
||||
print("")
|
||||
print(" ... Printing login response.")
|
||||
print("")
|
||||
print(blink.auth.login_response)
|
||||
print("")
|
||||
print(" ... Printing login attributes.")
|
||||
print("")
|
||||
print(blink.auth.login_attributes)
|
||||
print("")
|
||||
input(" ... Press any key to continue: ")
|
||||
print(" ... Deactivating auth token.")
|
||||
blink.auth.token = "foobar"
|
||||
print(f"\t - blink.auth.token = {blink.auth.token}")
|
||||
|
||||
print(" ... Attempting login.")
|
||||
print("")
|
||||
blink.start()
|
||||
print("")
|
||||
print(" ... Printing login response.")
|
||||
print("")
|
||||
print(blink.auth.login_response)
|
||||
print("")
|
||||
print(" ... Printing login attributes.")
|
||||
print("")
|
||||
print(blink.auth.login_attributes)
|
||||
print("")
|
||||
rint(" ... Done.")
|
||||
print("")
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../CHANGES.rst
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../CONTRIBUTING.rst
|
||||
@@ -0,0 +1,104 @@
|
||||
=======================
|
||||
Advanced Library Usage
|
||||
=======================
|
||||
|
||||
Usage of this library was designed with the `Home Assistant <https://home-assistant.io>`__ project in mind. With that said, this library is flexible to be used in other scripts where advanced usage not covered in the Quick Start guide may be required. This usage guide will attempt to cover as many use cases as possible.
|
||||
|
||||
Throttling
|
||||
--------------
|
||||
In general, attempting too many requests to the Blink servers will result in your account being throttled. Where possible, adding a delay between calls is ideal. For use cases where this is not an acceptable solution, the ``blinkpy.helpers.util`` module contains a ``Throttle`` class that can be used as a decorator for calls. There are many examples of usage within the ``blinkpy.api`` module. A simple example of usage is covered below, where the decorated method is prevented from executing again until 10s has passed. Note that if the method call is throttled by the decorator, the method will return `None`.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from blinkpy.helpers.util import Throttle
|
||||
|
||||
@Throttle(seconds=10)
|
||||
def my_method(*args):
|
||||
"""Some method to be throttled."""
|
||||
return True
|
||||
|
||||
Custom Sessions
|
||||
-----------------
|
||||
By default, the ``blink.auth.Auth`` class creates its own websession via its ``create_session`` method. This is done when the class is initialized and is accessible via the ``Auth.session`` property. To override with a custom websession, the following code can accomplish that:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from blinkpy.blinkpy import Blink
|
||||
from blinkpy.auth import Auth
|
||||
|
||||
blink = Blink()
|
||||
blink.auth = Auth()
|
||||
blink.auth.session = YourCustomSession
|
||||
|
||||
|
||||
Custom Retry Logic
|
||||
--------------------
|
||||
The built-in auth session via the ``create_session`` method allows for customizable retry intervals and conditions. These parameters are:
|
||||
|
||||
- retries
|
||||
- backoff
|
||||
- retry_list
|
||||
|
||||
``retries`` is the total number of retry attempts that each http request can do before timing out. ``backoff`` is a parameter that allows for non-linear retry times such that the time between retries is backoff*(2^(retries) - 1). ``retry_list`` is simply a list of status codes to force a retry. By default ``retries=3``, ``backoff=1``, and ``retry_list=[429, 500, 502, 503, 504]``. To override them, you need to add you overrides to a dictionary and use that to create a new session with the ``opts`` variable in the ``create_session`` method. The following example can serve as a guide where only the number of retries and backoff factor are overridden:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from blinkpy.blinkpy import Blink
|
||||
from blinkpy.auth import Auth
|
||||
|
||||
blink = Blink()
|
||||
blink.auth = Auth()
|
||||
|
||||
opts = {"retries": 10, "backoff": 2}
|
||||
blink.auth.session = blink.auth.create_session(opts=opts)
|
||||
|
||||
|
||||
Custom HTTP requests
|
||||
---------------------
|
||||
In addition to custom sessions, custom blink server requests can be performed. This give you the ability to bypass the built-in ``Auth.query`` method. It also allows flexibility by giving you the option to pass your own url, rather than be limited to what is currently implemented in the ``blinkpy.api`` module.
|
||||
|
||||
**Send custom url**
|
||||
This prepares a standard "GET" request.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from blinkpy.blinkpy import Blink
|
||||
from blinkpy.auth import Auth
|
||||
|
||||
blink = Blink()
|
||||
blink.auth = Auth()
|
||||
url = some_api_endpoint_string
|
||||
request = blink.auth.prepare_request(url, blink.auth.header, None, "get")
|
||||
response = blink.auth.session.send(request)
|
||||
|
||||
**Overload query method**
|
||||
Another option is to create your own ``Auth`` class with a custom ``query`` method to avoid the built-in response checking. This allows you to use the built in ``blinkpy.api`` endpoints, but also gives you flexibility to send your own urls.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from blinkpy.blinkpy import Blink
|
||||
from blinkpy.auth import Auth
|
||||
from blinkpy import api
|
||||
|
||||
class CustomAuth(Auth):
|
||||
def query(
|
||||
self,
|
||||
url=None,
|
||||
data=None,
|
||||
headers=self.header,
|
||||
reqtype="get",
|
||||
stream=False,
|
||||
json_resp=True,
|
||||
**kwargs
|
||||
):
|
||||
req = self.prepare_request(url, headers, data, reqtype)
|
||||
return self.session.send(req, stream=stream)
|
||||
|
||||
blink = blink.Blink()
|
||||
blink.auth = CustomAuth()
|
||||
|
||||
# Send custom GET query
|
||||
response = blink.auth.query(url=some_custom_url)
|
||||
|
||||
# Call built-in networks api endpoint
|
||||
response = api.request_networks(blink)
|
||||
@@ -1,17 +0,0 @@
|
||||
.. _core_module:
|
||||
|
||||
Blinkpy
|
||||
----------------------
|
||||
|
||||
.. automodule:: blinkpy.blinkpy
|
||||
:members:
|
||||
|
||||
.. automodule:: blinkpy.sync_module
|
||||
:members:
|
||||
|
||||
.. automodule:: blinkpy.camera
|
||||
:members:
|
||||
|
||||
.. automodule:: blinkpy.helpers.util
|
||||
:members:
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
API Reference
|
||||
----------------------
|
||||
|
||||
.. automodule:: blinkpy.api
|
||||
:members:
|
||||
|
||||
+6
-2
@@ -7,12 +7,16 @@ Welcome to blinkpy's documentation!
|
||||
===================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:maxdepth: 1
|
||||
:caption: Contents:
|
||||
:glob:
|
||||
|
||||
README
|
||||
api/*
|
||||
advanced
|
||||
CONTRIBUTING
|
||||
modules/*
|
||||
CHANGES
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
.. _core_module:
|
||||
|
||||
===========================
|
||||
Blinkpy Library Reference
|
||||
===========================
|
||||
|
||||
blinkpy.py
|
||||
-----------
|
||||
.. automodule:: blinkpy.blinkpy
|
||||
:members:
|
||||
|
||||
auth.py
|
||||
--------
|
||||
|
||||
.. automodule:: blinkpy.auth
|
||||
:members:
|
||||
|
||||
sync_module.py
|
||||
----------------
|
||||
|
||||
.. automodule:: blinkpy.sync_module
|
||||
:members:
|
||||
|
||||
camera.py
|
||||
-----------
|
||||
|
||||
.. automodule:: blinkpy.camera
|
||||
:members:
|
||||
|
||||
api.py
|
||||
---------
|
||||
|
||||
.. automodule:: blinkpy.api
|
||||
:members:
|
||||
|
||||
helpers/util.py
|
||||
----------------
|
||||
|
||||
.. automodule:: blinkpy.helpers.util
|
||||
:members:
|
||||
|
||||
+3
-4
@@ -1,4 +1,3 @@
|
||||
python-dateutil~=2.8.1
|
||||
requests~=2.23.0
|
||||
python-slugify~=4.0.0
|
||||
testtools==2.4.0
|
||||
python-dateutil>=2.8.1
|
||||
requests>=2.24.0
|
||||
python-slugify>=4.0.1
|
||||
|
||||
+14
-13
@@ -1,13 +1,14 @@
|
||||
black==19.10b0
|
||||
coverage==5.1
|
||||
flake8==3.8.1
|
||||
flake8-docstrings==1.5.0
|
||||
pre-commit==2.4.0
|
||||
pylint==2.5.2
|
||||
pydocstyle==5.0.2
|
||||
pytest==5.4.2
|
||||
pytest-cov==2.8.1
|
||||
pytest-sugar==0.9.3
|
||||
pytest-timeout==1.3.4
|
||||
restructuredtext-lint==1.3.0
|
||||
pygments==2.6.1
|
||||
black==21.6b0
|
||||
coverage==5.5
|
||||
flake8==3.9.1
|
||||
flake8-docstrings==1.6.0
|
||||
pre-commit==2.12.1
|
||||
pylint==2.8.2
|
||||
pydocstyle==6.0.0
|
||||
pytest==6.2.4
|
||||
pytest-cov==2.11.1
|
||||
pytest-sugar==0.9.4
|
||||
pytest-timeout==1.4.2
|
||||
restructuredtext-lint==1.3.2
|
||||
pygments==2.9.0
|
||||
testtools>=2.4.0
|
||||
|
||||
@@ -3,23 +3,26 @@
|
||||
from os.path import abspath, dirname
|
||||
from setuptools import setup, find_packages
|
||||
from blinkpy.helpers.constants import (
|
||||
__version__, PROJECT_PACKAGE_NAME, PROJECT_LICENSE, PROJECT_URL,
|
||||
PROJECT_EMAIL, PROJECT_DESCRIPTION, PROJECT_CLASSIFIERS, PROJECT_AUTHOR,
|
||||
__version__,
|
||||
PROJECT_PACKAGE_NAME,
|
||||
PROJECT_LICENSE,
|
||||
PROJECT_URL,
|
||||
PROJECT_EMAIL,
|
||||
PROJECT_DESCRIPTION,
|
||||
PROJECT_CLASSIFIERS,
|
||||
PROJECT_AUTHOR,
|
||||
)
|
||||
|
||||
PROJECT_VERSION = __version__
|
||||
|
||||
THIS_DIR = abspath(dirname(__file__))
|
||||
|
||||
REQUIRES = [
|
||||
"python-dateutil~=2.8.1",
|
||||
"requests~=2.23.0",
|
||||
"python-slugify~=4.0.0",
|
||||
]
|
||||
with open(f"{THIS_DIR}/requirements.txt") as req_file:
|
||||
REQUIRES = [line.rstrip() for line in req_file]
|
||||
|
||||
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()
|
||||
|
||||
setup(
|
||||
@@ -31,11 +34,11 @@ setup(
|
||||
author_email=PROJECT_EMAIL,
|
||||
license=PROJECT_LICENSE,
|
||||
url=PROJECT_URL,
|
||||
platforms='any',
|
||||
py_modules=['blinkpy'],
|
||||
platforms="any",
|
||||
py_modules=["blinkpy"],
|
||||
packages=PACKAGES,
|
||||
include_package_data=True,
|
||||
install_requires=REQUIRES,
|
||||
test_suite='tests',
|
||||
classifiers=PROJECT_CLASSIFIERS
|
||||
test_suite="tests",
|
||||
classifiers=PROJECT_CLASSIFIERS,
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ class MockResponse:
|
||||
self.json_data = json_data
|
||||
self.status_code = status_code
|
||||
self.raw_data = raw_data
|
||||
self.reason = "foobar"
|
||||
|
||||
def json(self):
|
||||
"""Return json data from get_request."""
|
||||
|
||||
+104
-12
@@ -3,7 +3,13 @@
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from requests import exceptions
|
||||
from blinkpy.auth import Auth, LoginError, TokenRefreshFailed
|
||||
from blinkpy.auth import (
|
||||
Auth,
|
||||
LoginError,
|
||||
TokenRefreshFailed,
|
||||
BlinkBadResponse,
|
||||
UnauthorizedError,
|
||||
)
|
||||
import blinkpy.helpers.constants as const
|
||||
import tests.mock_responses as mresp
|
||||
|
||||
@@ -36,7 +42,6 @@ class TestAuth(unittest.TestCase):
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"uid": 1234,
|
||||
"notification_key": 1234,
|
||||
"device_id": const.DEVICE_ID,
|
||||
}
|
||||
self.assertDictEqual(auth.data, expected_data)
|
||||
@@ -56,7 +61,6 @@ class TestAuth(unittest.TestCase):
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"uid": 1234,
|
||||
"notification_key": 1234,
|
||||
"device_id": const.DEVICE_ID,
|
||||
}
|
||||
self.assertDictEqual(auth.data, expected_data)
|
||||
@@ -86,25 +90,47 @@ class TestAuth(unittest.TestCase):
|
||||
|
||||
def test_bad_response_code(self):
|
||||
"""Check bad response code from server."""
|
||||
self.auth.is_errored = False
|
||||
fake_resp = mresp.MockResponse({"code": 404}, 404)
|
||||
with self.assertRaises(exceptions.ConnectionError):
|
||||
self.auth.validate_response(fake_resp, True)
|
||||
self.assertTrue(self.auth.is_errored)
|
||||
|
||||
self.auth.is_errored = False
|
||||
fake_resp = mresp.MockResponse({"code": 101}, 401)
|
||||
with self.assertRaises(UnauthorizedError):
|
||||
self.auth.validate_response(fake_resp, True)
|
||||
self.assertTrue(self.auth.is_errored)
|
||||
|
||||
def test_good_response_code(self):
|
||||
"""Check good response code from server."""
|
||||
fake_resp = mresp.MockResponse({"foo": "bar"}, 200)
|
||||
self.auth.is_errored = True
|
||||
self.assertEqual(self.auth.validate_response(fake_resp, True), {"foo": "bar"})
|
||||
self.assertFalse(self.auth.is_errored)
|
||||
|
||||
def test_response_not_json(self):
|
||||
"""Check response when not json."""
|
||||
fake_resp = "foobar"
|
||||
self.auth.is_errored = True
|
||||
self.assertEqual(self.auth.validate_response(fake_resp, False), "foobar")
|
||||
self.assertFalse(self.auth.is_errored)
|
||||
|
||||
def test_response_bad_json(self):
|
||||
"""Check response when not json but expecting json."""
|
||||
self.auth.is_errored = False
|
||||
with self.assertRaises(BlinkBadResponse):
|
||||
self.auth.validate_response(None, True)
|
||||
self.assertTrue(self.auth.is_errored)
|
||||
|
||||
def test_header(self):
|
||||
"""Test header data."""
|
||||
self.auth.token = "bar"
|
||||
self.auth.host = "foo"
|
||||
expected_header = {"Host": "foo", "TOKEN_AUTH": "bar"}
|
||||
expected_header = {
|
||||
"TOKEN_AUTH": "bar",
|
||||
"user-agent": const.DEFAULT_USER_AGENT,
|
||||
"content-type": "application/json",
|
||||
}
|
||||
self.assertDictEqual(self.auth.header, expected_header)
|
||||
|
||||
def test_header_no_token(self):
|
||||
@@ -126,17 +152,19 @@ class TestAuth(unittest.TestCase):
|
||||
"""Test login handling when bad response."""
|
||||
fake_resp = mresp.MockResponse({"foo": "bar"}, 404)
|
||||
mock_req.return_value = fake_resp
|
||||
self.auth.is_errored = False
|
||||
with self.assertRaises(LoginError):
|
||||
self.auth.login()
|
||||
with self.assertRaises(TokenRefreshFailed):
|
||||
self.auth.refresh_token()
|
||||
self.assertTrue(self.auth.is_errored)
|
||||
|
||||
@mock.patch("blinkpy.auth.Auth.login")
|
||||
def test_refresh_token(self, mock_login):
|
||||
"""Test refresh token method."""
|
||||
mock_login.return_value = {
|
||||
"region": {"tier": "test"},
|
||||
"authtoken": {"authtoken": "foobar"},
|
||||
"client": {"id": 1234},
|
||||
"account": {"id": 5678},
|
||||
"account": {"account_id": 5678, "client_id": 1234, "tier": "test"},
|
||||
"auth": {"token": "foobar"},
|
||||
}
|
||||
self.assertTrue(self.auth.refresh_token())
|
||||
self.assertEqual(self.auth.region_id, "test")
|
||||
@@ -148,20 +176,29 @@ class TestAuth(unittest.TestCase):
|
||||
def test_refresh_token_failed(self, mock_login):
|
||||
"""Test refresh token failed."""
|
||||
mock_login.return_value = {}
|
||||
self.auth.is_errored = False
|
||||
with self.assertRaises(TokenRefreshFailed):
|
||||
self.auth.refresh_token()
|
||||
self.assertTrue(self.auth.is_errored)
|
||||
|
||||
def test_check_key_required(self):
|
||||
"""Check key required method."""
|
||||
self.auth.login_response = {}
|
||||
self.assertFalse(self.auth.check_key_required())
|
||||
|
||||
self.auth.login_response = {"client": {"verification_required": False}}
|
||||
self.auth.login_response = {"account": {"client_verification_required": False}}
|
||||
self.assertFalse(self.auth.check_key_required())
|
||||
|
||||
self.auth.login_response = {"client": {"verification_required": True}}
|
||||
self.auth.login_response = {"account": {"client_verification_required": True}}
|
||||
self.assertTrue(self.auth.check_key_required())
|
||||
|
||||
@mock.patch("blinkpy.auth.api.request_logout")
|
||||
def test_logout(self, mock_req):
|
||||
"""Test logout method."""
|
||||
mock_blink = MockBlink(None)
|
||||
mock_req.return_value = True
|
||||
self.assertTrue(self.auth.logout(mock_blink))
|
||||
|
||||
@mock.patch("blinkpy.auth.api.request_verify")
|
||||
def test_send_auth_key(self, mock_req):
|
||||
"""Check sending of auth key."""
|
||||
@@ -192,10 +229,65 @@ class TestAuth(unittest.TestCase):
|
||||
def test_query_retry(self, mock_refresh, mock_validate):
|
||||
"""Check handling of request retry."""
|
||||
self.auth.session = MockSession()
|
||||
mock_validate.side_effect = [TokenRefreshFailed, "foobar"]
|
||||
mock_validate.side_effect = [UnauthorizedError, "foobar"]
|
||||
mock_refresh.return_value = True
|
||||
self.assertEqual(self.auth.query(url="http://example.com"), "foobar")
|
||||
|
||||
@mock.patch("blinkpy.auth.Auth.validate_response")
|
||||
@mock.patch("blinkpy.auth.Auth.refresh_token")
|
||||
def test_query_retry_failed(self, mock_refresh, mock_validate):
|
||||
"""Check handling of failed retry request."""
|
||||
self.auth.session = MockSession()
|
||||
mock_validate.side_effect = [UnauthorizedError, BlinkBadResponse]
|
||||
mock_refresh.return_value = True
|
||||
self.assertEqual(self.auth.query(url="http://example.com"), None)
|
||||
|
||||
mock_validate.side_effect = [UnauthorizedError, TokenRefreshFailed]
|
||||
self.assertEqual(self.auth.query(url="http://example.com"), None)
|
||||
|
||||
def test_default_session(self):
|
||||
"""Test default session creation."""
|
||||
sess = self.auth.create_session()
|
||||
adapter = sess.adapters["https://"]
|
||||
self.assertEqual(adapter.max_retries.total, 3)
|
||||
self.assertEqual(adapter.max_retries.backoff_factor, 1)
|
||||
self.assertEqual(
|
||||
adapter.max_retries.status_forcelist, [429, 500, 502, 503, 504]
|
||||
)
|
||||
|
||||
def test_custom_session_full(self):
|
||||
"""Test full custom session creation."""
|
||||
opts = {"backoff": 2, "retries": 10, "retry_list": [404]}
|
||||
sess = self.auth.create_session(opts=opts)
|
||||
adapter = sess.adapters["https://"]
|
||||
self.assertEqual(adapter.max_retries.total, 10)
|
||||
self.assertEqual(adapter.max_retries.backoff_factor, 2)
|
||||
self.assertEqual(adapter.max_retries.status_forcelist, [404])
|
||||
|
||||
def test_custom_session_partial(self):
|
||||
"""Test partial custom session creation."""
|
||||
opts1 = {"backoff": 2}
|
||||
opts2 = {"retries": 5}
|
||||
opts3 = {"retry_list": [101, 202]}
|
||||
sess1 = self.auth.create_session(opts=opts1)
|
||||
sess2 = self.auth.create_session(opts=opts2)
|
||||
sess3 = self.auth.create_session(opts=opts3)
|
||||
adapt1 = sess1.adapters["https://"]
|
||||
adapt2 = sess2.adapters["https://"]
|
||||
adapt3 = sess3.adapters["https://"]
|
||||
|
||||
self.assertEqual(adapt1.max_retries.total, 3)
|
||||
self.assertEqual(adapt1.max_retries.backoff_factor, 2)
|
||||
self.assertEqual(adapt1.max_retries.status_forcelist, [429, 500, 502, 503, 504])
|
||||
|
||||
self.assertEqual(adapt2.max_retries.total, 5)
|
||||
self.assertEqual(adapt2.max_retries.backoff_factor, 1)
|
||||
self.assertEqual(adapt2.max_retries.status_forcelist, [429, 500, 502, 503, 504])
|
||||
|
||||
self.assertEqual(adapt3.max_retries.total, 3)
|
||||
self.assertEqual(adapt3.max_retries.backoff_factor, 1)
|
||||
self.assertEqual(adapt3.max_retries.status_forcelist, [101, 202])
|
||||
|
||||
|
||||
class MockSession:
|
||||
"""Object to mock a session."""
|
||||
|
||||
@@ -2,32 +2,27 @@
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import logging
|
||||
import time
|
||||
|
||||
from blinkpy import blinkpy
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.camera import BlinkCamera
|
||||
from blinkpy.helpers.util import get_time, BlinkURLHandler
|
||||
|
||||
|
||||
class MockSyncModule(BlinkSyncModule):
|
||||
"""Mock http requests from sync module."""
|
||||
"""Mock blink sync module object."""
|
||||
|
||||
def __init__(self, blink, header):
|
||||
"""Create mock sync module instance."""
|
||||
super().__init__(blink, header, network_id=None, camera_list=None)
|
||||
self.blink = blink
|
||||
self.header = header
|
||||
self.return_value = None
|
||||
self.return_value2 = None
|
||||
def get_network_info(self):
|
||||
"""Mock network info method."""
|
||||
return True
|
||||
|
||||
def http_get(self, url, stream=False, json=True):
|
||||
"""Mock get request."""
|
||||
if stream and self.return_value2 is not None:
|
||||
return self.return_value2
|
||||
return self.return_value
|
||||
|
||||
def http_post(self, url):
|
||||
"""Mock post request."""
|
||||
return self.return_value
|
||||
class MockCamera(BlinkCamera):
|
||||
"""Mock blink camera object."""
|
||||
|
||||
def update(self, config, force_cache=False, **kwargs):
|
||||
"""Mock camera update method."""
|
||||
|
||||
|
||||
class TestBlinkFunctions(unittest.TestCase):
|
||||
@@ -94,9 +89,33 @@ class TestBlinkFunctions(unittest.TestCase):
|
||||
"DEBUG:blinkpy.blinkpy:foo: /bar.mp4 is marked as deleted.",
|
||||
]
|
||||
with self.assertLogs() as dl_log:
|
||||
blink.download_videos("/tmp", stop=2)
|
||||
blink.download_videos("/tmp", stop=2, delay=0)
|
||||
self.assertEqual(dl_log.output, expected_log)
|
||||
|
||||
@mock.patch("blinkpy.blinkpy.api.request_videos")
|
||||
def test_parse_downloaded_throttle(self, mock_req):
|
||||
"""Test ability to parse downloaded items list."""
|
||||
generic_entry = {
|
||||
"created_at": "1970",
|
||||
"device_name": "foo",
|
||||
"deleted": False,
|
||||
"media": "/bar.mp4",
|
||||
}
|
||||
result = [generic_entry]
|
||||
mock_req.return_value = {"media": result}
|
||||
self.blink.last_refresh = 0
|
||||
start = time.time()
|
||||
self.blink.download_videos("/tmp", stop=2, delay=0, debug=True)
|
||||
now = time.time()
|
||||
delta = now - start
|
||||
self.assertTrue(delta < 0.1)
|
||||
|
||||
start = time.time()
|
||||
self.blink.download_videos("/tmp", stop=2, delay=0.1, debug=True)
|
||||
now = time.time()
|
||||
delta = now - start
|
||||
self.assertTrue(delta >= 0.1)
|
||||
|
||||
@mock.patch("blinkpy.blinkpy.api.request_videos")
|
||||
def test_parse_camera_not_in_list(self, mock_req):
|
||||
"""Test ability to parse downloaded items list."""
|
||||
@@ -119,5 +138,18 @@ class TestBlinkFunctions(unittest.TestCase):
|
||||
"DEBUG:blinkpy.blinkpy:Skipping videos for foo.",
|
||||
]
|
||||
with self.assertLogs() as dl_log:
|
||||
blink.download_videos("/tmp", camera="bar", stop=2)
|
||||
blink.download_videos("/tmp", camera="bar", stop=2, delay=0)
|
||||
self.assertEqual(dl_log.output, expected_log)
|
||||
|
||||
@mock.patch("blinkpy.blinkpy.api.request_network_update")
|
||||
@mock.patch("blinkpy.auth.Auth.query")
|
||||
def test_refresh(self, mock_req, mock_update):
|
||||
"""Test ability to refresh system."""
|
||||
mock_update.return_value = {"network": {"sync_module_error": False}}
|
||||
mock_req.return_value = None
|
||||
self.blink.last_refresh = 0
|
||||
self.blink.available = True
|
||||
self.blink.sync["foo"] = MockSyncModule(self.blink, "foo", 1, [])
|
||||
self.blink.cameras = {"bar": MockCamera(self.blink.sync)}
|
||||
self.blink.sync["foo"].cameras = self.blink.cameras
|
||||
self.assertTrue(self.blink.refresh())
|
||||
|
||||
+117
-10
@@ -9,6 +9,7 @@ any communication related errors at startup.
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from blinkpy.blinkpy import Blink, BlinkSetupError
|
||||
from blinkpy.sync_module import BlinkOwl
|
||||
from blinkpy.helpers.constants import __version__
|
||||
|
||||
|
||||
@@ -67,8 +68,8 @@ class TestBlinkSetup(unittest.TestCase):
|
||||
self.assertEqual(self.blink.last_refresh, None)
|
||||
with mock.patch(
|
||||
"blinkpy.sync_module.BlinkSyncModule.refresh", return_value=True
|
||||
):
|
||||
self.blink.refresh()
|
||||
), mock.patch("blinkpy.blinkpy.Blink.get_homescreen", return_value=True):
|
||||
self.blink.refresh(force=True)
|
||||
|
||||
self.assertEqual(self.blink.last_refresh, now)
|
||||
self.assertEqual(self.blink.check_if_ok_to_update(), False)
|
||||
@@ -81,14 +82,21 @@ class TestBlinkSetup(unittest.TestCase):
|
||||
self.assertEqual(self.blink.sync["TEST"], 1234)
|
||||
self.assertEqual(self.blink.sync["tEsT"], 1234)
|
||||
|
||||
@mock.patch("blinkpy.api.request_camera_usage")
|
||||
@mock.patch("blinkpy.api.request_homescreen")
|
||||
def test_setup_cameras(self, mock_home):
|
||||
def test_setup_cameras(self, mock_home, mock_req):
|
||||
"""Check retrieval of camera information."""
|
||||
mock_home.return_value = {
|
||||
"cameras": [
|
||||
{"name": "foo", "network_id": 1234, "id": 5678},
|
||||
{"name": "bar", "network_id": 1234, "id": 5679},
|
||||
{"name": "test", "network_id": 4321, "id": 0000},
|
||||
mock_home.return_value = {}
|
||||
mock_req.return_value = {
|
||||
"networks": [
|
||||
{
|
||||
"network_id": 1234,
|
||||
"cameras": [
|
||||
{"id": 5678, "name": "foo"},
|
||||
{"id": 5679, "name": "bar"},
|
||||
],
|
||||
},
|
||||
{"network_id": 4321, "cameras": [{"id": 0000, "name": "test"}]},
|
||||
]
|
||||
}
|
||||
result = self.blink.setup_camera_list()
|
||||
@@ -100,12 +108,15 @@ class TestBlinkSetup(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
@mock.patch("blinkpy.api.request_homescreen")
|
||||
@mock.patch("blinkpy.api.request_camera_usage")
|
||||
def test_setup_cameras_failure(self, mock_home):
|
||||
"""Check that on failure we raise a setup error."""
|
||||
mock_home.return_value = {}
|
||||
with self.assertRaises(BlinkSetupError):
|
||||
self.blink.setup_camera_list()
|
||||
mock_home.return_value = None
|
||||
with self.assertRaises(BlinkSetupError):
|
||||
self.blink.setup_camera_list()
|
||||
|
||||
def test_setup_urls(self):
|
||||
"""Check setup of URLS."""
|
||||
@@ -132,6 +143,9 @@ class TestBlinkSetup(unittest.TestCase):
|
||||
mock_networks.return_value = {}
|
||||
with self.assertRaises(BlinkSetupError):
|
||||
self.blink.setup_networks()
|
||||
mock_networks.return_value = None
|
||||
with self.assertRaises(BlinkSetupError):
|
||||
self.blink.setup_networks()
|
||||
|
||||
@mock.patch("blinkpy.blinkpy.Auth.send_auth_key")
|
||||
def test_setup_prompt_2fa(self, mock_key):
|
||||
@@ -149,10 +163,12 @@ class TestBlinkSetup(unittest.TestCase):
|
||||
|
||||
@mock.patch("blinkpy.blinkpy.Blink.setup_camera_list")
|
||||
@mock.patch("blinkpy.api.request_networks")
|
||||
def test_setup_post_verify(self, mock_networks, mock_camera):
|
||||
@mock.patch("blinkpy.blinkpy.Blink.setup_owls")
|
||||
def test_setup_post_verify(self, mock_owl, mock_networks, mock_camera):
|
||||
"""Test setup after verification."""
|
||||
self.blink.available = False
|
||||
self.blink.key_required = True
|
||||
mock_owl.return_value = True
|
||||
mock_networks.return_value = {
|
||||
"summary": {"foo": {"onboarded": False, "name": "bar"}}
|
||||
}
|
||||
@@ -181,6 +197,97 @@ class TestBlinkSetup(unittest.TestCase):
|
||||
self.assertEqual(combined["fizz"], "buzz")
|
||||
self.assertEqual(combined["bar"], "foo")
|
||||
|
||||
@mock.patch("blinkpy.blinkpy.BlinkOwl.start")
|
||||
def test_initialize_blink_minis(self, mock_start):
|
||||
"""Test blink mini initialization."""
|
||||
mock_start.return_value = True
|
||||
self.blink.homescreen = {
|
||||
"owls": [
|
||||
{
|
||||
"enabled": False,
|
||||
"id": 1,
|
||||
"name": "foo",
|
||||
"network_id": 2,
|
||||
"onboarded": True,
|
||||
"status": "online",
|
||||
"thumbnail": "/foo/bar",
|
||||
"serial": "1234",
|
||||
},
|
||||
{
|
||||
"enabled": True,
|
||||
"id": 3,
|
||||
"name": "bar",
|
||||
"network_id": 4,
|
||||
"onboarded": True,
|
||||
"status": "online",
|
||||
"thumbnail": "/foo/bar",
|
||||
"serial": "abcd",
|
||||
},
|
||||
]
|
||||
}
|
||||
self.blink.sync = {}
|
||||
self.blink.setup_owls()
|
||||
self.assertEqual(self.blink.sync["foo"].__class__, BlinkOwl)
|
||||
self.assertEqual(self.blink.sync["bar"].__class__, BlinkOwl)
|
||||
self.assertEqual(self.blink.sync["foo"].arm, False)
|
||||
self.assertEqual(self.blink.sync["bar"].arm, True)
|
||||
self.assertEqual(self.blink.sync["foo"].name, "foo")
|
||||
self.assertEqual(self.blink.sync["bar"].name, "bar")
|
||||
|
||||
def test_blink_mini_cameras_returned(self):
|
||||
"""Test that blink mini cameras are found if attached to sync module."""
|
||||
self.blink.network_ids = ["1234"]
|
||||
self.blink.homescreen = {
|
||||
"owls": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "foo",
|
||||
"network_id": 1234,
|
||||
"onboarded": True,
|
||||
"enabled": True,
|
||||
"status": "online",
|
||||
"thumbnail": "/foo/bar",
|
||||
"serial": "abc123",
|
||||
}
|
||||
]
|
||||
}
|
||||
result = self.blink.setup_owls()
|
||||
self.assertEqual(self.blink.network_ids, ["1234"])
|
||||
self.assertEqual(
|
||||
result, [{"1234": {"name": "foo", "id": "1234", "type": "mini"}}]
|
||||
)
|
||||
|
||||
self.blink.no_owls = True
|
||||
self.blink.network_ids = []
|
||||
self.blink.get_homescreen()
|
||||
result = self.blink.setup_owls()
|
||||
self.assertEqual(self.blink.network_ids, [])
|
||||
self.assertEqual(result, [])
|
||||
|
||||
@mock.patch("blinkpy.api.request_camera_usage")
|
||||
def test_blink_mini_attached_to_sync(self, mock_usage):
|
||||
"""Test that blink mini cameras are properly attached to sync module."""
|
||||
self.blink.network_ids = ["1234"]
|
||||
self.blink.homescreen = {
|
||||
"owls": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "foo",
|
||||
"network_id": 1234,
|
||||
"onboarded": True,
|
||||
"enabled": True,
|
||||
"status": "online",
|
||||
"thumbnail": "/foo/bar",
|
||||
"serial": "abc123",
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_usage.return_value = {"networks": [{"cameras": [], "network_id": 1234}]}
|
||||
result = self.blink.setup_camera_list()
|
||||
self.assertEqual(
|
||||
result, {"1234": [{"name": "foo", "id": "1234", "type": "mini"}]}
|
||||
)
|
||||
|
||||
|
||||
class MockSync:
|
||||
"""Mock sync module class."""
|
||||
|
||||
+36
-5
@@ -11,7 +11,7 @@ from unittest import mock
|
||||
from blinkpy.blinkpy import Blink
|
||||
from blinkpy.helpers.util import BlinkURLHandler
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.camera import BlinkCamera
|
||||
from blinkpy.camera import BlinkCamera, BlinkCameraMini
|
||||
|
||||
|
||||
CAMERA_CFG = {
|
||||
@@ -117,10 +117,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
"WARNING:blinkpy.camera:Could not retrieve calibrated "
|
||||
"temperature."
|
||||
),
|
||||
(
|
||||
"WARNING:blinkpy.camera:Could not find thumbnail for camera new"
|
||||
"\nNoneType: None"
|
||||
),
|
||||
("WARNING:blinkpy.camera:Could not find thumbnail for camera new"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -144,6 +141,15 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
self.assertEqual(self.camera.clip, None)
|
||||
self.assertEqual(self.camera.video_from_cache, None)
|
||||
|
||||
def test_camera_arm_status(self, mock_resp):
|
||||
"""Test arming and disarming camera."""
|
||||
self.camera.motion_enabled = None
|
||||
self.assertFalse(self.camera.arm)
|
||||
self.camera.motion_enabled = False
|
||||
self.assertFalse(self.camera.arm)
|
||||
self.camera.motion_enabled = True
|
||||
self.assertTrue(self.camera.arm)
|
||||
|
||||
@mock.patch("blinkpy.camera.api.request_motion_detection_enable")
|
||||
@mock.patch("blinkpy.camera.api.request_motion_detection_disable")
|
||||
def test_motion_detection_enable_disable(self, mock_dis, mock_en, mock_rep):
|
||||
@@ -152,3 +158,28 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
mock_en.return_value = "enable"
|
||||
self.assertEqual(self.camera.set_motion_detect(True), "enable")
|
||||
self.assertEqual(self.camera.set_motion_detect(False), "disable")
|
||||
|
||||
def test_missing_attributes(self, mock_resp):
|
||||
"""Test that attributes return None if missing."""
|
||||
self.camera.temperature = None
|
||||
self.camera.serial = None
|
||||
attr = self.camera.attributes
|
||||
self.assertEqual(attr["serial"], None)
|
||||
self.assertEqual(attr["temperature"], None)
|
||||
self.assertEqual(attr["temperature_c"], None)
|
||||
|
||||
def test_mini_missing_attributes(self, mock_resp):
|
||||
"""Test that attributes return None if missing."""
|
||||
camera = BlinkCameraMini(self.blink.sync)
|
||||
self.blink.sync.network_id = None
|
||||
self.blink.sync.name = None
|
||||
attr = camera.attributes
|
||||
for key in attr:
|
||||
self.assertEqual(attr[key], None)
|
||||
|
||||
def test_camera_stream(self, mock_resp):
|
||||
"""Test that camera stream returns correct url."""
|
||||
mock_resp.return_value = {"server": "rtsps://foo.bar"}
|
||||
mini_camera = BlinkCameraMini(self.blink.sync["test"])
|
||||
self.assertEqual(self.camera.get_liveview(), "rtsps://foo.bar")
|
||||
self.assertEqual(mini_camera.get_liveview(), "rtsps://foo.bar")
|
||||
|
||||
@@ -4,8 +4,8 @@ from unittest import mock
|
||||
|
||||
from blinkpy.blinkpy import Blink
|
||||
from blinkpy.helpers.util import BlinkURLHandler
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.camera import BlinkCamera
|
||||
from blinkpy.sync_module import BlinkSyncModule, BlinkOwl
|
||||
from blinkpy.camera import BlinkCamera, BlinkCameraMini
|
||||
|
||||
|
||||
@mock.patch("blinkpy.auth.Auth.query")
|
||||
@@ -78,13 +78,31 @@ class TestBlinkSyncModule(unittest.TestCase):
|
||||
self.assertEqual(self.blink.sync["test"].get_camera_info("1234"), "foobar")
|
||||
|
||||
def test_get_camera_info_fail(self, mock_resp):
|
||||
"""Test hadnling of failed get camera info function."""
|
||||
"""Test handling of failed get camera info function."""
|
||||
mock_resp.return_value = None
|
||||
self.assertEqual(self.blink.sync["test"].get_camera_info("1"), [])
|
||||
self.assertEqual(self.blink.sync["test"].get_camera_info("1"), {})
|
||||
mock_resp.return_value = {}
|
||||
self.assertEqual(self.blink.sync["test"].get_camera_info("1"), [])
|
||||
self.assertEqual(self.blink.sync["test"].get_camera_info("1"), {})
|
||||
mock_resp.return_value = {"camera": None}
|
||||
self.assertEqual(self.blink.sync["test"].get_camera_info("1"), [])
|
||||
self.assertEqual(self.blink.sync["test"].get_camera_info("1"), {})
|
||||
|
||||
def test_get_network_info(self, mock_resp):
|
||||
"""Test network retrieval."""
|
||||
mock_resp.return_value = {"network": {"sync_module_error": False}}
|
||||
self.assertTrue(self.blink.sync["test"].get_network_info())
|
||||
mock_resp.return_value = {"network": {"sync_module_error": True}}
|
||||
self.assertFalse(self.blink.sync["test"].get_network_info())
|
||||
|
||||
def test_get_network_info_failure(self, mock_resp):
|
||||
"""Test failed network retrieval."""
|
||||
mock_resp.return_value = {}
|
||||
self.blink.sync["test"].available = True
|
||||
self.assertFalse(self.blink.sync["test"].get_network_info())
|
||||
self.assertFalse(self.blink.sync["test"].available)
|
||||
self.blink.sync["test"].available = True
|
||||
mock_resp.return_value = None
|
||||
self.assertFalse(self.blink.sync["test"].get_network_info())
|
||||
self.assertFalse(self.blink.sync["test"].available)
|
||||
|
||||
def test_check_new_videos_startup(self, mock_resp):
|
||||
"""Test that check_new_videos does not block startup."""
|
||||
@@ -257,3 +275,20 @@ class TestBlinkSyncModule(unittest.TestCase):
|
||||
"""Test sync attributes."""
|
||||
self.assertEqual(self.blink.sync["test"].attributes["name"], "test")
|
||||
self.assertEqual(self.blink.sync["test"].attributes["network_id"], "1234")
|
||||
|
||||
def test_owl_start(self, mock_resp):
|
||||
"""Test owl camera instantiation."""
|
||||
response = {
|
||||
"name": "foo",
|
||||
"id": 2,
|
||||
"serial": "foobar123",
|
||||
"enabled": True,
|
||||
"network_id": 1,
|
||||
"thumbnail": "/foo/bar",
|
||||
}
|
||||
self.blink.last_refresh = None
|
||||
self.blink.homescreen = {"owls": [response]}
|
||||
owl = BlinkOwl(self.blink, "foo", 1234, response)
|
||||
self.assertTrue(owl.start())
|
||||
self.assertTrue("foo" in owl.cameras)
|
||||
self.assertEqual(owl.cameras["foo"].__class__, BlinkCameraMini)
|
||||
|
||||
+36
-1
@@ -3,7 +3,7 @@
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import time
|
||||
from blinkpy.helpers.util import json_load, Throttle, time_to_seconds
|
||||
from blinkpy.helpers.util import json_load, Throttle, time_to_seconds, gen_uid
|
||||
|
||||
|
||||
class TestUtil(unittest.TestCase):
|
||||
@@ -67,6 +67,25 @@ class TestUtil(unittest.TestCase):
|
||||
self.assertEqual(throttled(), True)
|
||||
self.assertEqual(throttled(), None)
|
||||
|
||||
def test_throttle_multiple_objects(self):
|
||||
"""Test that function is throttled even if called by multiple objects."""
|
||||
|
||||
@Throttle(seconds=5)
|
||||
def test_throttle_method():
|
||||
return True
|
||||
|
||||
class Tester:
|
||||
"""A tester class for throttling."""
|
||||
|
||||
def test(self):
|
||||
"""Test function for throttle."""
|
||||
return test_throttle_method()
|
||||
|
||||
tester1 = Tester()
|
||||
tester2 = Tester()
|
||||
self.assertEqual(tester1.test(), True)
|
||||
self.assertEqual(tester2.test(), None)
|
||||
|
||||
def test_throttle_on_two_methods(self):
|
||||
"""Test that throttle works for multiple methods."""
|
||||
|
||||
@@ -113,3 +132,19 @@ class TestUtil(unittest.TestCase):
|
||||
self.assertEqual(json_load("fake.file"), None)
|
||||
with mock.patch("builtins.open", mock.mock_open(read_data="")):
|
||||
self.assertEqual(json_load("fake.file"), None)
|
||||
|
||||
def test_gen_uid(self):
|
||||
"""Test gen_uid formatting."""
|
||||
val1 = gen_uid(8)
|
||||
val2 = gen_uid(8, uid_format=True)
|
||||
|
||||
self.assertEqual(len(val1), 16)
|
||||
|
||||
self.assertTrue(val2.startswith("BlinkCamera_"))
|
||||
val2_cut = val2.split("_")
|
||||
val2_split = val2_cut[1].split("-")
|
||||
self.assertEqual(len(val2_split[0]), 8)
|
||||
self.assertEqual(len(val2_split[1]), 4)
|
||||
self.assertEqual(len(val2_split[2]), 4)
|
||||
self.assertEqual(len(val2_split[3]), 4)
|
||||
self.assertEqual(len(val2_split[4]), 12)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[tox]
|
||||
envlist = build, py36, py37, py38, lint
|
||||
envlist = build, py36, py37, py38, py39, lint
|
||||
skip_missing_interpreters = True
|
||||
skipsdist = True
|
||||
|
||||
@@ -41,8 +41,7 @@ commands =
|
||||
flake8 blinkpy tests app
|
||||
pydocstyle blinkpy tests app
|
||||
black --check --diff blinkpy tests app
|
||||
rst-lint README.rst
|
||||
rst-lint CHANGES.rst
|
||||
rst-lint README.rst CHANGES.rst CONTRIBUTING.rst
|
||||
|
||||
[testenv:build]
|
||||
recreate = True
|
||||
|
||||
Reference in New Issue
Block a user