Compare commits
173
Commits
v0.16.0-rc9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff306b87d2 | ||
|
|
9d9d3aa7a3 | ||
|
|
3ddfd10678 | ||
|
|
7c75ffa815 | ||
|
|
4e4b22314a | ||
|
|
4e6b570cba | ||
|
|
b12d9ba969 | ||
|
|
83df95f0da | ||
|
|
3c5caae93f | ||
|
|
1ced64de65 | ||
|
|
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 | ||
|
|
37350da1b2 | ||
|
|
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 | ||
|
|
19b7eef123 | ||
|
|
41d59b1df0 | ||
|
|
fd104c09da | ||
|
|
6dbed91426 | ||
|
|
cb43f17d75 | ||
|
|
5ebf278ad3 | ||
|
|
6bd8d14190 | ||
|
|
7f782533b8 | ||
|
|
1790c1beae | ||
|
|
09e1c382d3 | ||
|
|
ceb2270ee5 | ||
|
|
986a3c81f3 | ||
|
|
11d6bbb916 | ||
|
|
b02f6318a7 | ||
|
|
c1435e74e8 | ||
|
|
b30da661e5 | ||
|
|
3a22dddc82 | ||
|
|
a19a17d4dc | ||
|
|
bff6ef92ce | ||
|
|
f6ddfbe5cf | ||
|
|
263572eb66 | ||
|
|
3a97a928de | ||
|
|
0322414de1 | ||
|
|
1c75e86863 | ||
|
|
0ae71f6367 | ||
|
|
af7c38695a | ||
|
|
c741c22876 | ||
|
|
06b9b12eeb | ||
|
|
daf7078f98 | ||
|
|
c2db57ecb3 | ||
|
|
1b66eddfb4 | ||
|
|
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 |
+3
-2
@@ -1,5 +1,5 @@
|
|||||||
# Number of days of inactivity before an issue becomes stale
|
# 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
|
# Number of days of inactivity before a stale issue is closed
|
||||||
daysUntilClose: 7
|
daysUntilClose: 7
|
||||||
@@ -8,7 +8,8 @@ daysUntilClose: 7
|
|||||||
exemptLabels:
|
exemptLabels:
|
||||||
- Priority
|
- Priority
|
||||||
- bug
|
- bug
|
||||||
- Help Wanted
|
- help wanted
|
||||||
|
- feature request
|
||||||
|
|
||||||
# Set to true to ignore issues in a project (defaults to false)
|
# Set to true to ignore issues in a project (defaults to false)
|
||||||
exemptProjects: true
|
exemptProjects: true
|
||||||
|
|||||||
@@ -33,8 +33,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
tox -r -e cov
|
tox -r -e cov
|
||||||
- name: Codecov
|
- name: Codecov
|
||||||
uses: codecov/codecov-action@v1.0.6
|
uses: codecov/codecov-action@v1
|
||||||
with:
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
flags: unittests
|
flags: unittests
|
||||||
file: ./coverage.xml
|
file: ./coverage.xml
|
||||||
name: blinkpy
|
name: blinkpy
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [3.8]
|
python-version: [3.9]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v1
|
||||||
with:
|
with:
|
||||||
python-version: '3.7'
|
python-version: '3.8'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
strategy:
|
strategy:
|
||||||
max-parallel: 3
|
max-parallel: 4
|
||||||
matrix:
|
matrix:
|
||||||
platform:
|
platform:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
python-version: [3.6, 3.7, 3.8]
|
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ build/*
|
|||||||
docs/_build
|
docs/_build
|
||||||
*.log
|
*.log
|
||||||
venv
|
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,12 +8,12 @@ repos:
|
|||||||
- --quiet
|
- --quiet
|
||||||
files: ^((blinkpy|tests)/.+)?[^/]+\.py$
|
files: ^((blinkpy|tests)/.+)?[^/]+\.py$
|
||||||
- repo: https://gitlab.com/pycqa/flake8
|
- repo: https://gitlab.com/pycqa/flake8
|
||||||
rev: 3.8.2
|
rev: 3.8.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- flake8-docstrings==1.5.0
|
- flake8-docstrings==1.5.0
|
||||||
- pydocstyle==5.0.2
|
- pydocstyle==5.1.1
|
||||||
files: ^(blinkpy|tests)/.+\.py$
|
files: ^(blinkpy|tests)/.+\.py$
|
||||||
- repo: https://github.com/Lucas-C/pre-commit-hooks-markup
|
- repo: https://github.com/Lucas-C/pre-commit-hooks-markup
|
||||||
rev: v1.0.0
|
rev: v1.0.0
|
||||||
|
|||||||
+105
@@ -4,6 +4,111 @@ Changelog
|
|||||||
|
|
||||||
A list of changes between each release
|
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)
|
0.15.0 (2020-05-08)
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
**Breaking Changes:**
|
**Breaking Changes:**
|
||||||
|
|||||||
+3
-3
@@ -174,13 +174,13 @@ Similar methods exist for individual cameras:
|
|||||||
|
|
||||||
Download videos
|
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.
|
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:
|
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
|
.. code:: python
|
||||||
|
|
||||||
blink.download_videos('/home/blink', since='2018/07/04 09:34')
|
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
|
.. |Build Status| image:: https://github.com/fronzbot/blinkpy/workflows/build/badge.svg
|
||||||
|
|||||||
+17
-12
@@ -3,7 +3,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from blinkpy.helpers.util import get_time, Throttle
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -20,21 +20,22 @@ def request_login(
|
|||||||
:param url: Login url.
|
:param url: Login url.
|
||||||
:login_data: Dictionary containing blink login data.
|
: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(
|
data = dumps(
|
||||||
{
|
{
|
||||||
"email": login_data["username"],
|
"email": login_data["username"],
|
||||||
"password": login_data["password"],
|
"password": login_data["password"],
|
||||||
"notification_key": login_data["notification_key"],
|
|
||||||
"unique_id": login_data["uid"],
|
"unique_id": login_data["uid"],
|
||||||
"app_version": "6.0.7 (520300) #afb0be72a",
|
|
||||||
"device_identifier": login_data["device_id"],
|
"device_identifier": login_data["device_id"],
|
||||||
"client_name": "Computer",
|
"client_name": "Computer",
|
||||||
"client_type": "android",
|
"reauth": True,
|
||||||
"os_version": "5.1.1",
|
|
||||||
"reauth": "false",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return auth.query(
|
return auth.query(
|
||||||
url=url,
|
url=url,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
@@ -54,6 +55,12 @@ def request_verify(auth, blink, verify_key):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
def request_networks(blink):
|
||||||
"""Request all networks information."""
|
"""Request all networks information."""
|
||||||
url = f"{blink.urls.base_url}/networks"
|
url = f"{blink.urls.base_url}/networks"
|
||||||
@@ -241,9 +248,7 @@ def request_camera_liveview(blink, network, camera_id):
|
|||||||
:param network: Sync module network id.
|
:param network: Sync module network id.
|
||||||
:param camera_id: Camera ID of camera to request liveview from.
|
:param camera_id: Camera ID of camera to request liveview from.
|
||||||
"""
|
"""
|
||||||
url = (
|
url = f"{blink.urls.base_url}/api/v5/accounts/{blink.account_id}/networks/{network}/cameras/{camera_id}/liveview"
|
||||||
f"{blink.urls.base_url}/api/v3/networks/{network}/cameras/{camera_id}/liveview"
|
|
||||||
)
|
|
||||||
return http_post(blink, url)
|
return http_post(blink, url)
|
||||||
|
|
||||||
|
|
||||||
@@ -284,7 +289,7 @@ def request_motion_detection_disable(blink, network, camera_id):
|
|||||||
return http_post(blink, url)
|
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.
|
Perform an http get request.
|
||||||
|
|
||||||
@@ -304,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.
|
Perform an http post request.
|
||||||
|
|
||||||
|
|||||||
+72
-31
@@ -2,9 +2,16 @@
|
|||||||
import logging
|
import logging
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from requests import Request, Session, exceptions
|
from requests import Request, Session, exceptions
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
from urllib3.util.retry import Retry
|
||||||
from blinkpy import api
|
from blinkpy import api
|
||||||
from blinkpy.helpers import util
|
from blinkpy.helpers import util
|
||||||
from blinkpy.helpers.constants import BLINK_URL, LOGIN_ENDPOINT
|
from blinkpy.helpers.constants import (
|
||||||
|
BLINK_URL,
|
||||||
|
DEFAULT_USER_AGENT,
|
||||||
|
LOGIN_ENDPOINT,
|
||||||
|
TIMEOUT,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -12,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class Auth:
|
class Auth:
|
||||||
"""Class to handle login communication."""
|
"""Class to handle login communication."""
|
||||||
|
|
||||||
def __init__(self, login_data=None, no_prompt=False, login_method="v4"):
|
def __init__(self, login_data=None, no_prompt=False):
|
||||||
"""
|
"""
|
||||||
Initialize auth handler.
|
Initialize auth handler.
|
||||||
|
|
||||||
@@ -22,7 +29,6 @@ class Auth:
|
|||||||
- password
|
- password
|
||||||
:param no_prompt: Should any user input prompts
|
:param no_prompt: Should any user input prompts
|
||||||
be supressed? True/FALSE
|
be supressed? True/FALSE
|
||||||
:param login_method: Choose the login endpoint to use. Default: v4. v3 uses email verification rather than a 2FA code.
|
|
||||||
"""
|
"""
|
||||||
if login_data is None:
|
if login_data is None:
|
||||||
login_data = {}
|
login_data = {}
|
||||||
@@ -32,7 +38,6 @@ class Auth:
|
|||||||
self.region_id = login_data.get("region_id", None)
|
self.region_id = login_data.get("region_id", None)
|
||||||
self.client_id = login_data.get("client_id", None)
|
self.client_id = login_data.get("client_id", None)
|
||||||
self.account_id = login_data.get("account_id", None)
|
self.account_id = login_data.get("account_id", None)
|
||||||
self.login_url = LOGIN_ENDPOINT[login_method]
|
|
||||||
self.login_response = None
|
self.login_response = None
|
||||||
self.is_errored = False
|
self.is_errored = False
|
||||||
self.no_prompt = no_prompt
|
self.no_prompt = no_prompt
|
||||||
@@ -53,12 +58,31 @@ class Auth:
|
|||||||
"""Return authorization header."""
|
"""Return authorization header."""
|
||||||
if self.token is None:
|
if self.token is None:
|
||||||
return 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."""
|
"""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 = 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
|
return sess
|
||||||
|
|
||||||
def prepare_request(self, url, headers, data, reqtype):
|
def prepare_request(self, url, headers, data, reqtype):
|
||||||
@@ -75,17 +99,21 @@ class Auth:
|
|||||||
|
|
||||||
self.data = util.validate_login_data(self.data)
|
self.data = util.validate_login_data(self.data)
|
||||||
|
|
||||||
def login(self):
|
def login(self, login_url=LOGIN_ENDPOINT):
|
||||||
"""Attempt login to blink servers."""
|
"""Attempt login to blink servers."""
|
||||||
self.validate_login()
|
self.validate_login()
|
||||||
_LOGGER.info("Attempting login with %s", self.login_url)
|
_LOGGER.info("Attempting login with %s", login_url)
|
||||||
response = api.request_login(self, self.login_url, self.data, is_retry=False,)
|
response = api.request_login(self, login_url, self.data, is_retry=False,)
|
||||||
try:
|
try:
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return response.json()
|
return response.json()
|
||||||
raise LoginError
|
raise LoginError
|
||||||
except AttributeError:
|
except AttributeError as error:
|
||||||
raise LoginError
|
raise LoginError from error
|
||||||
|
|
||||||
|
def logout(self, blink):
|
||||||
|
"""Log out."""
|
||||||
|
return api.request_logout(blink)
|
||||||
|
|
||||||
def refresh_token(self):
|
def refresh_token(self):
|
||||||
"""Refresh auth token."""
|
"""Refresh auth token."""
|
||||||
@@ -93,20 +121,24 @@ class Auth:
|
|||||||
try:
|
try:
|
||||||
_LOGGER.info("Token expired, attempting automatic refresh.")
|
_LOGGER.info("Token expired, attempting automatic refresh.")
|
||||||
self.login_response = self.login()
|
self.login_response = self.login()
|
||||||
self.region_id = self.login_response["region"]["tier"]
|
self.extract_login_info()
|
||||||
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"]
|
|
||||||
self.is_errored = False
|
self.is_errored = False
|
||||||
except LoginError:
|
except LoginError as error:
|
||||||
_LOGGER.error("Login endpoint failed. Try again later.")
|
_LOGGER.error("Login endpoint failed. Try again later.")
|
||||||
raise TokenRefreshFailed
|
raise TokenRefreshFailed from error
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError) as error:
|
||||||
_LOGGER.error("Malformed login response: %s", self.login_response)
|
_LOGGER.error("Malformed login response: %s", self.login_response)
|
||||||
raise TokenRefreshFailed
|
raise TokenRefreshFailed from error
|
||||||
return True
|
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):
|
def startup(self):
|
||||||
"""Initialize tokens for communication."""
|
"""Initialize tokens for communication."""
|
||||||
self.validate_login()
|
self.validate_login()
|
||||||
@@ -127,8 +159,8 @@ class Auth:
|
|||||||
json_data = response.json()
|
json_data = response.json()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
except (AttributeError, ValueError):
|
except (AttributeError, ValueError) as error:
|
||||||
raise BlinkBadResponse
|
raise BlinkBadResponse from error
|
||||||
|
|
||||||
self.is_errored = False
|
self.is_errored = False
|
||||||
return json_data
|
return json_data
|
||||||
@@ -142,6 +174,7 @@ class Auth:
|
|||||||
stream=False,
|
stream=False,
|
||||||
json_resp=True,
|
json_resp=True,
|
||||||
is_retry=False,
|
is_retry=False,
|
||||||
|
timeout=TIMEOUT,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Perform server requests.
|
Perform server requests.
|
||||||
@@ -156,21 +189,25 @@ class Auth:
|
|||||||
"""
|
"""
|
||||||
req = self.prepare_request(url, headers, data, reqtype)
|
req = self.prepare_request(url, headers, data, reqtype)
|
||||||
try:
|
try:
|
||||||
response = self.session.send(req, stream=stream)
|
response = self.session.send(req, stream=stream, timeout=timeout)
|
||||||
return self.validate_response(response, json_resp)
|
return self.validate_response(response, json_resp)
|
||||||
except (exceptions.ConnectionError, exceptions.Timeout):
|
except (exceptions.ConnectionError, exceptions.Timeout):
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Connection error. Endpoint %s possibly down or throttled. %s: %s",
|
"Connection error. Endpoint %s possibly down or throttled.", url,
|
||||||
url,
|
|
||||||
response.status_code,
|
|
||||||
response.reason,
|
|
||||||
)
|
)
|
||||||
except BlinkBadResponse:
|
except BlinkBadResponse:
|
||||||
|
code = None
|
||||||
|
reason = None
|
||||||
|
try:
|
||||||
|
code = response.status_code
|
||||||
|
reason = response.reason
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Expected json response from %s, but received: %s: %s",
|
"Expected json response from %s, but received: %s: %s",
|
||||||
url,
|
url,
|
||||||
response.status_code,
|
code,
|
||||||
response.reason,
|
reason,
|
||||||
)
|
)
|
||||||
except UnauthorizedError:
|
except UnauthorizedError:
|
||||||
try:
|
try:
|
||||||
@@ -184,6 +221,7 @@ class Auth:
|
|||||||
stream=stream,
|
stream=stream,
|
||||||
json_resp=json_resp,
|
json_resp=json_resp,
|
||||||
is_retry=True,
|
is_retry=True,
|
||||||
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
_LOGGER.error("Unable to access %s after token refresh.", url)
|
_LOGGER.error("Unable to access %s after token refresh.", url)
|
||||||
except TokenRefreshFailed:
|
except TokenRefreshFailed:
|
||||||
@@ -197,6 +235,9 @@ class Auth:
|
|||||||
try:
|
try:
|
||||||
json_resp = response.json()
|
json_resp = response.json()
|
||||||
blink.available = json_resp["valid"]
|
blink.available = json_resp["valid"]
|
||||||
|
if not json_resp["valid"]:
|
||||||
|
_LOGGER.error("%s", json_resp["message"])
|
||||||
|
return False
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
_LOGGER.error("Did not receive valid response from server.")
|
_LOGGER.error("Did not receive valid response from server.")
|
||||||
return False
|
return False
|
||||||
@@ -205,7 +246,7 @@ class Auth:
|
|||||||
def check_key_required(self):
|
def check_key_required(self):
|
||||||
"""Check if 2FA key is required."""
|
"""Check if 2FA key is required."""
|
||||||
try:
|
try:
|
||||||
if self.login_response["client"]["verification_required"]:
|
if self.login_response["account"]["client_verification_required"]:
|
||||||
return True
|
return True
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
|||||||
+23
-9
@@ -29,6 +29,7 @@ from blinkpy.helpers.constants import (
|
|||||||
DEFAULT_MOTION_INTERVAL,
|
DEFAULT_MOTION_INTERVAL,
|
||||||
DEFAULT_REFRESH,
|
DEFAULT_REFRESH,
|
||||||
MIN_THROTTLE_TIME,
|
MIN_THROTTLE_TIME,
|
||||||
|
TIMEOUT_MEDIA,
|
||||||
)
|
)
|
||||||
from blinkpy.helpers.constants import __version__
|
from blinkpy.helpers.constants import __version__
|
||||||
from blinkpy.auth import Auth, TokenRefreshFailed, LoginError
|
from blinkpy.auth import Auth, TokenRefreshFailed, LoginError
|
||||||
@@ -75,20 +76,22 @@ class Blink:
|
|||||||
self.no_owls = no_owls
|
self.no_owls = no_owls
|
||||||
|
|
||||||
@util.Throttle(seconds=MIN_THROTTLE_TIME)
|
@util.Throttle(seconds=MIN_THROTTLE_TIME)
|
||||||
def refresh(self, force=False):
|
def refresh(self, force=False, force_cache=False):
|
||||||
"""
|
"""
|
||||||
Perform a system refresh.
|
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:
|
if not self.available:
|
||||||
self.setup_post_verify()
|
self.setup_post_verify()
|
||||||
|
|
||||||
|
self.get_homescreen()
|
||||||
for sync_name, sync_module in self.sync.items():
|
for sync_name, sync_module in self.sync.items():
|
||||||
_LOGGER.debug("Attempting refresh of sync %s", sync_name)
|
_LOGGER.debug("Attempting refresh of sync %s", sync_name)
|
||||||
sync_module.refresh(force_cache=force)
|
sync_module.refresh(force_cache=(force or force_cache))
|
||||||
if not force:
|
if not force_cache:
|
||||||
# Prevents rapid clearing of motion detect property
|
# Prevents rapid clearing of motion detect property
|
||||||
self.last_refresh = int(time.time())
|
self.last_refresh = int(time.time())
|
||||||
return True
|
return True
|
||||||
@@ -261,7 +264,9 @@ class Blink:
|
|||||||
"""Save login data to file."""
|
"""Save login data to file."""
|
||||||
util.json_save(self.auth.login_attributes, file_name)
|
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.
|
Download all videos from server since specified time.
|
||||||
|
|
||||||
@@ -272,6 +277,7 @@ class Blink:
|
|||||||
:param camera: Camera name to retrieve. Defaults to "all".
|
:param camera: Camera name to retrieve. Defaults to "all".
|
||||||
Use a list for multiple cameras.
|
Use a list for multiple cameras.
|
||||||
:param stop: Page to stop on (~25 items per page. Default page 10).
|
: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.
|
:param debug: Set to TRUE to prevent downloading of items.
|
||||||
Instead of downloading, entries will be printed to log.
|
Instead of downloading, entries will be printed to log.
|
||||||
"""
|
"""
|
||||||
@@ -298,9 +304,9 @@ class Blink:
|
|||||||
_LOGGER.info("No videos found on page %s. Exiting.", page)
|
_LOGGER.info("No videos found on page %s. Exiting.", page)
|
||||||
break
|
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."""
|
"""Parse downloaded videos."""
|
||||||
for item in result:
|
for item in result:
|
||||||
try:
|
try:
|
||||||
@@ -330,7 +336,13 @@ class Blink:
|
|||||||
_LOGGER.info("%s already exists, skipping...", filename)
|
_LOGGER.info("%s already exists, skipping...", filename)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
response = api.http_get(self, url=clip_address, 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:
|
with open(filename, "wb") as vidfile:
|
||||||
copyfileobj(response.raw, vidfile)
|
copyfileobj(response.raw, vidfile)
|
||||||
|
|
||||||
@@ -342,6 +354,8 @@ class Blink:
|
|||||||
"Address: {address}, Filename: {filename}"
|
"Address: {address}, Filename: {filename}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if delay > 0:
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
|
|
||||||
class BlinkSetupError(Exception):
|
class BlinkSetupError(Exception):
|
||||||
|
|||||||
+21
-12
@@ -3,6 +3,7 @@
|
|||||||
from shutil import copyfileobj
|
from shutil import copyfileobj
|
||||||
import logging
|
import logging
|
||||||
from blinkpy import api
|
from blinkpy import api
|
||||||
|
from blinkpy.helpers.constants import TIMEOUT_MEDIA
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -97,6 +98,19 @@ class BlinkCamera:
|
|||||||
self.sync.blink, self.network_id, self.camera_id
|
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):
|
def snap_picture(self):
|
||||||
"""Take a picture with camera to create a new thumbnail."""
|
"""Take a picture with camera to create a new thumbnail."""
|
||||||
return api.request_new_image(self.sync.blink, self.network_id, self.camera_id)
|
return api.request_new_image(self.sync.blink, self.network_id, self.camera_id)
|
||||||
@@ -150,9 +164,7 @@ class BlinkCamera:
|
|||||||
if config.get("thumbnail", False):
|
if config.get("thumbnail", False):
|
||||||
thumb_addr = config["thumbnail"]
|
thumb_addr = config["thumbnail"]
|
||||||
else:
|
else:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning("Could not find thumbnail for camera %s", self.name)
|
||||||
"Could not find thumbnail for camera %s", self.name, exc_info=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if thumb_addr is not None:
|
if thumb_addr is not None:
|
||||||
new_thumbnail = f"{self.sync.urls.base_url}{thumb_addr}.jpg"
|
new_thumbnail = f"{self.sync.urls.base_url}{thumb_addr}.jpg"
|
||||||
@@ -181,13 +193,10 @@ class BlinkCamera:
|
|||||||
update_cached_video = True
|
update_cached_video = True
|
||||||
|
|
||||||
if new_thumbnail is not None and (update_cached_image or force_cache):
|
if new_thumbnail is not None and (update_cached_image or force_cache):
|
||||||
self._cached_image = api.http_get(
|
self._cached_image = self.get_media()
|
||||||
self.sync.blink, url=self.thumbnail, stream=True, json=False
|
|
||||||
)
|
|
||||||
if clip_addr is not None and (update_cached_video or force_cache):
|
if clip_addr is not None and (update_cached_video or force_cache):
|
||||||
self._cached_video = api.http_get(
|
self._cached_video = self.get_media(media_type="video")
|
||||||
self.sync.blink, url=self.clip, stream=True, json=False
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_liveview(self):
|
def get_liveview(self):
|
||||||
"""Get livewview rtsps link."""
|
"""Get livewview rtsps link."""
|
||||||
@@ -203,7 +212,7 @@ class BlinkCamera:
|
|||||||
:param path: Path to write file
|
:param path: Path to write file
|
||||||
"""
|
"""
|
||||||
_LOGGER.debug("Writing image from %s to %s", self.name, path)
|
_LOGGER.debug("Writing image from %s to %s", self.name, path)
|
||||||
response = self._cached_image
|
response = self.get_media()
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
with open(path, "wb") as imgfile:
|
with open(path, "wb") as imgfile:
|
||||||
copyfileobj(response.raw, imgfile)
|
copyfileobj(response.raw, imgfile)
|
||||||
@@ -219,7 +228,7 @@ class BlinkCamera:
|
|||||||
:param path: Path to write file
|
:param path: Path to write file
|
||||||
"""
|
"""
|
||||||
_LOGGER.debug("Writing video from %s to %s", self.name, path)
|
_LOGGER.debug("Writing video from %s to %s", self.name, path)
|
||||||
response = self._cached_video
|
response = self.get_media(media_type="video")
|
||||||
if response is None:
|
if response is None:
|
||||||
_LOGGER.error("No saved video exist for %s.", self.name)
|
_LOGGER.error("No saved video exist for %s.", self.name)
|
||||||
return
|
return
|
||||||
@@ -261,6 +270,6 @@ class BlinkCameraMini(BlinkCamera):
|
|||||||
response = api.http_post(self.sync.blink, url)
|
response = api.http_post(self.sync.blink, url)
|
||||||
server = response["server"]
|
server = response["server"]
|
||||||
server_split = server.split(":")
|
server_split = server.split(":")
|
||||||
server_split[0] = "rtsps"
|
server_split[0] = "rtsps:"
|
||||||
link = "".join(server_split)
|
link = "".join(server_split)
|
||||||
return link
|
return link
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
MAJOR_VERSION = 0
|
MAJOR_VERSION = 0
|
||||||
MINOR_VERSION = 16
|
MINOR_VERSION = 17
|
||||||
PATCH_VERSION = "0-rc9"
|
PATCH_VERSION = 1
|
||||||
|
|
||||||
__version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}"
|
__version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}"
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ PROJECT_CLASSIFIERS = [
|
|||||||
"Programming Language :: Python :: 3.6",
|
"Programming Language :: Python :: 3.6",
|
||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
"Topic :: Home Automation",
|
"Topic :: Home Automation",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -48,10 +49,7 @@ URLS
|
|||||||
BLINK_URL = "immedia-semi.com"
|
BLINK_URL = "immedia-semi.com"
|
||||||
DEFAULT_URL = f"rest-prod.{BLINK_URL}"
|
DEFAULT_URL = f"rest-prod.{BLINK_URL}"
|
||||||
BASE_URL = f"https://{DEFAULT_URL}"
|
BASE_URL = f"https://{DEFAULT_URL}"
|
||||||
LOGIN_ENDPOINT = {
|
LOGIN_ENDPOINT = f"{BASE_URL}/api/v5/account/login"
|
||||||
"v4": f"{BASE_URL}/api/v4/account/login",
|
|
||||||
"v3": f"{BASE_URL}/api/v3/login",
|
|
||||||
}
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Dictionaries
|
Dictionaries
|
||||||
@@ -61,6 +59,7 @@ ONLINE = {"online": True, "offline": False}
|
|||||||
"""
|
"""
|
||||||
OTHER
|
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"
|
DEVICE_ID = "Blinkpy"
|
||||||
TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
|
TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
|
||||||
DEFAULT_MOTION_INTERVAL = 1
|
DEFAULT_MOTION_INTERVAL = 1
|
||||||
@@ -68,3 +67,5 @@ DEFAULT_REFRESH = 30
|
|||||||
MIN_THROTTLE_TIME = 2
|
MIN_THROTTLE_TIME = 2
|
||||||
SIZE_NOTIFICATION_KEY = 152
|
SIZE_NOTIFICATION_KEY = 152
|
||||||
SIZE_UID = 16
|
SIZE_UID = 16
|
||||||
|
TIMEOUT = 10
|
||||||
|
TIMEOUT_MEDIA = 90
|
||||||
|
|||||||
@@ -33,10 +33,13 @@ def json_save(data, file_name):
|
|||||||
json.dump(data, json_file, indent=4)
|
json.dump(data, json_file, indent=4)
|
||||||
|
|
||||||
|
|
||||||
def gen_uid(size):
|
def gen_uid(size, uid_format=False):
|
||||||
"""Create a random sring."""
|
"""Create a random sring."""
|
||||||
full_token = secrets.token_hex(size)
|
if uid_format:
|
||||||
return full_token[0:size]
|
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):
|
def time_to_seconds(timestamp):
|
||||||
@@ -79,10 +82,7 @@ def prompt_login_data(data):
|
|||||||
|
|
||||||
def validate_login_data(data):
|
def validate_login_data(data):
|
||||||
"""Check for missing keys."""
|
"""Check for missing keys."""
|
||||||
data["uid"] = data.get("uid", gen_uid(const.SIZE_UID))
|
data["uid"] = data.get("uid", gen_uid(const.SIZE_UID, uid_format=True))
|
||||||
data["notification_key"] = data.get(
|
|
||||||
"notification_key", gen_uid(const.SIZE_NOTIFICATION_KEY)
|
|
||||||
)
|
|
||||||
data["device_id"] = data.get("device_id", const.DEVICE_ID)
|
data["device_id"] = data.get("device_id", const.DEVICE_ID)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -93,9 +93,7 @@ class BlinkSyncModule:
|
|||||||
self.serial = self.summary["serial"]
|
self.serial = self.summary["serial"]
|
||||||
self.status = self.summary["status"]
|
self.status = self.summary["status"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
_LOGGER.error(
|
_LOGGER.error("Could not extract some sync module info: %s", response)
|
||||||
"Could not extract some sync module info: %s", response, exc_info=True
|
|
||||||
)
|
|
||||||
|
|
||||||
is_ok = self.get_network_info()
|
is_ok = self.get_network_info()
|
||||||
self.check_new_videos()
|
self.check_new_videos()
|
||||||
@@ -147,7 +145,7 @@ class BlinkSyncModule:
|
|||||||
for owl in self.blink.homescreen["owls"]:
|
for owl in self.blink.homescreen["owls"]:
|
||||||
if owl["name"] == name:
|
if owl["name"] == name:
|
||||||
return owl
|
return owl
|
||||||
except KeyError:
|
except (TypeError, KeyError):
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -158,7 +156,7 @@ class BlinkSyncModule:
|
|||||||
try:
|
try:
|
||||||
return response["event"]
|
return response["event"]
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError):
|
||||||
_LOGGER.error("Could not extract events: %s", response, exc_info=True)
|
_LOGGER.error("Could not extract events: %s", response)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_camera_info(self, camera_id, **kwargs):
|
def get_camera_info(self, camera_id, **kwargs):
|
||||||
@@ -170,7 +168,7 @@ class BlinkSyncModule:
|
|||||||
try:
|
try:
|
||||||
return response["camera"][0]
|
return response["camera"][0]
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError):
|
||||||
_LOGGER.error("Could not extract camera info: %s", response, exc_info=True)
|
_LOGGER.error("Could not extract camera info: %s", response)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_network_info(self):
|
def get_network_info(self):
|
||||||
@@ -272,7 +270,7 @@ class BlinkOwl(BlinkSyncModule):
|
|||||||
if owl["name"] == self.name:
|
if owl["name"] == self.name:
|
||||||
self.status = owl["enabled"]
|
self.status = owl["enabled"]
|
||||||
return owl
|
return owl
|
||||||
except KeyError:
|
except (TypeError, KeyError):
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -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("")
|
||||||
@@ -30,6 +30,29 @@ By default, the ``blink.auth.Auth`` class creates its own websession via its ``c
|
|||||||
blink.auth = Auth()
|
blink.auth = Auth()
|
||||||
blink.auth.session = YourCustomSession
|
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
|
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.
|
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.
|
||||||
|
|||||||
+3
-4
@@ -1,4 +1,3 @@
|
|||||||
python-dateutil~=2.8.1
|
python-dateutil>=2.8.1
|
||||||
requests~=2.24.0
|
requests>=2.24.0
|
||||||
python-slugify~=4.0.0
|
python-slugify>=4.0.1
|
||||||
testtools==2.4.0
|
|
||||||
|
|||||||
+12
-11
@@ -1,13 +1,14 @@
|
|||||||
black==19.10b0
|
black==19.10b0
|
||||||
coverage==5.1
|
coverage==5.4
|
||||||
flake8==3.8.3
|
flake8==3.8.4
|
||||||
flake8-docstrings==1.5.0
|
flake8-docstrings==1.5.0
|
||||||
pre-commit==2.5.1
|
pre-commit==2.10.1
|
||||||
pylint==2.5.3
|
pylint==2.6.2
|
||||||
pydocstyle==5.0.2
|
pydocstyle==5.1.1
|
||||||
pytest==5.4.3
|
pytest==6.2.2
|
||||||
pytest-cov==2.10.0
|
pytest-cov==2.11.1
|
||||||
pytest-sugar==0.9.3
|
pytest-sugar==0.9.4
|
||||||
pytest-timeout==1.4.1
|
pytest-timeout==1.4.2
|
||||||
restructuredtext-lint==1.3.1
|
restructuredtext-lint==1.3.2
|
||||||
pygments==2.6.1
|
pygments==2.8.0
|
||||||
|
testtools>=2.4.0
|
||||||
|
|||||||
@@ -3,23 +3,26 @@
|
|||||||
from os.path import abspath, dirname
|
from os.path import abspath, dirname
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
from blinkpy.helpers.constants import (
|
from blinkpy.helpers.constants import (
|
||||||
__version__, PROJECT_PACKAGE_NAME, PROJECT_LICENSE, PROJECT_URL,
|
__version__,
|
||||||
PROJECT_EMAIL, PROJECT_DESCRIPTION, PROJECT_CLASSIFIERS, PROJECT_AUTHOR,
|
PROJECT_PACKAGE_NAME,
|
||||||
|
PROJECT_LICENSE,
|
||||||
|
PROJECT_URL,
|
||||||
|
PROJECT_EMAIL,
|
||||||
|
PROJECT_DESCRIPTION,
|
||||||
|
PROJECT_CLASSIFIERS,
|
||||||
|
PROJECT_AUTHOR,
|
||||||
)
|
)
|
||||||
|
|
||||||
PROJECT_VERSION = __version__
|
PROJECT_VERSION = __version__
|
||||||
|
|
||||||
THIS_DIR = abspath(dirname(__file__))
|
THIS_DIR = abspath(dirname(__file__))
|
||||||
|
|
||||||
REQUIRES = [
|
with open(f"{THIS_DIR}/requirements.txt") as req_file:
|
||||||
"python-dateutil~=2.8.1",
|
REQUIRES = [line.rstrip() for line in req_file]
|
||||||
"requests~=2.23.0",
|
|
||||||
"python-slugify~=4.0.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
PACKAGES = find_packages(exclude=['tests*', 'docs'])
|
PACKAGES = find_packages(exclude=["tests*", "docs"])
|
||||||
|
|
||||||
with open('{}/README.rst'.format(THIS_DIR), encoding='utf-8') as readme_file:
|
with open("{}/README.rst".format(THIS_DIR), encoding="utf-8") as readme_file:
|
||||||
LONG_DESCRIPTION = readme_file.read()
|
LONG_DESCRIPTION = readme_file.read()
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
@@ -31,11 +34,11 @@ setup(
|
|||||||
author_email=PROJECT_EMAIL,
|
author_email=PROJECT_EMAIL,
|
||||||
license=PROJECT_LICENSE,
|
license=PROJECT_LICENSE,
|
||||||
url=PROJECT_URL,
|
url=PROJECT_URL,
|
||||||
platforms='any',
|
platforms="any",
|
||||||
py_modules=['blinkpy'],
|
py_modules=["blinkpy"],
|
||||||
packages=PACKAGES,
|
packages=PACKAGES,
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=REQUIRES,
|
install_requires=REQUIRES,
|
||||||
test_suite='tests',
|
test_suite="tests",
|
||||||
classifiers=PROJECT_CLASSIFIERS
|
classifiers=PROJECT_CLASSIFIERS,
|
||||||
)
|
)
|
||||||
|
|||||||
+60
-26
@@ -42,7 +42,6 @@ class TestAuth(unittest.TestCase):
|
|||||||
"username": "foo",
|
"username": "foo",
|
||||||
"password": "bar",
|
"password": "bar",
|
||||||
"uid": 1234,
|
"uid": 1234,
|
||||||
"notification_key": 1234,
|
|
||||||
"device_id": const.DEVICE_ID,
|
"device_id": const.DEVICE_ID,
|
||||||
}
|
}
|
||||||
self.assertDictEqual(auth.data, expected_data)
|
self.assertDictEqual(auth.data, expected_data)
|
||||||
@@ -62,7 +61,6 @@ class TestAuth(unittest.TestCase):
|
|||||||
"username": "foo",
|
"username": "foo",
|
||||||
"password": "bar",
|
"password": "bar",
|
||||||
"uid": 1234,
|
"uid": 1234,
|
||||||
"notification_key": 1234,
|
|
||||||
"device_id": const.DEVICE_ID,
|
"device_id": const.DEVICE_ID,
|
||||||
}
|
}
|
||||||
self.assertDictEqual(auth.data, expected_data)
|
self.assertDictEqual(auth.data, expected_data)
|
||||||
@@ -128,8 +126,11 @@ class TestAuth(unittest.TestCase):
|
|||||||
def test_header(self):
|
def test_header(self):
|
||||||
"""Test header data."""
|
"""Test header data."""
|
||||||
self.auth.token = "bar"
|
self.auth.token = "bar"
|
||||||
self.auth.host = "foo"
|
expected_header = {
|
||||||
expected_header = {"Host": "foo", "TOKEN_AUTH": "bar"}
|
"TOKEN_AUTH": "bar",
|
||||||
|
"user-agent": const.DEFAULT_USER_AGENT,
|
||||||
|
"content-type": "application/json",
|
||||||
|
}
|
||||||
self.assertDictEqual(self.auth.header, expected_header)
|
self.assertDictEqual(self.auth.header, expected_header)
|
||||||
|
|
||||||
def test_header_no_token(self):
|
def test_header_no_token(self):
|
||||||
@@ -144,21 +145,6 @@ class TestAuth(unittest.TestCase):
|
|||||||
fake_resp = mresp.MockResponse({"foo": "bar"}, 200)
|
fake_resp = mresp.MockResponse({"foo": "bar"}, 200)
|
||||||
mock_req.return_value = fake_resp
|
mock_req.return_value = fake_resp
|
||||||
self.assertEqual(self.auth.login(), {"foo": "bar"})
|
self.assertEqual(self.auth.login(), {"foo": "bar"})
|
||||||
mock_req.assert_called_with(
|
|
||||||
self.auth, const.LOGIN_ENDPOINT["v4"], {}, is_retry=False
|
|
||||||
)
|
|
||||||
|
|
||||||
@mock.patch("blinkpy.auth.Auth.validate_login", return_value=None)
|
|
||||||
@mock.patch("blinkpy.auth.api.request_login")
|
|
||||||
def test_login_v3(self, mock_req, mock_validate):
|
|
||||||
"""Test login handling."""
|
|
||||||
auth_v3 = Auth(login_method="v3")
|
|
||||||
fake_resp = mresp.MockResponse({"foo": "bar"}, 200)
|
|
||||||
mock_req.return_value = fake_resp
|
|
||||||
self.assertEqual(auth_v3.login(), {"foo": "bar"})
|
|
||||||
mock_req.assert_called_with(
|
|
||||||
auth_v3, const.LOGIN_ENDPOINT["v3"], {}, is_retry=False
|
|
||||||
)
|
|
||||||
|
|
||||||
@mock.patch("blinkpy.auth.Auth.validate_login", return_value=None)
|
@mock.patch("blinkpy.auth.Auth.validate_login", return_value=None)
|
||||||
@mock.patch("blinkpy.auth.api.request_login")
|
@mock.patch("blinkpy.auth.api.request_login")
|
||||||
@@ -177,10 +163,8 @@ class TestAuth(unittest.TestCase):
|
|||||||
def test_refresh_token(self, mock_login):
|
def test_refresh_token(self, mock_login):
|
||||||
"""Test refresh token method."""
|
"""Test refresh token method."""
|
||||||
mock_login.return_value = {
|
mock_login.return_value = {
|
||||||
"region": {"tier": "test"},
|
"account": {"account_id": 5678, "client_id": 1234, "tier": "test"},
|
||||||
"authtoken": {"authtoken": "foobar"},
|
"auth": {"token": "foobar"},
|
||||||
"client": {"id": 1234},
|
|
||||||
"account": {"id": 5678},
|
|
||||||
}
|
}
|
||||||
self.assertTrue(self.auth.refresh_token())
|
self.assertTrue(self.auth.refresh_token())
|
||||||
self.assertEqual(self.auth.region_id, "test")
|
self.assertEqual(self.auth.region_id, "test")
|
||||||
@@ -202,12 +186,19 @@ class TestAuth(unittest.TestCase):
|
|||||||
self.auth.login_response = {}
|
self.auth.login_response = {}
|
||||||
self.assertFalse(self.auth.check_key_required())
|
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.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())
|
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")
|
@mock.patch("blinkpy.auth.api.request_verify")
|
||||||
def test_send_auth_key(self, mock_req):
|
def test_send_auth_key(self, mock_req):
|
||||||
"""Check sending of auth key."""
|
"""Check sending of auth key."""
|
||||||
@@ -246,7 +237,7 @@ class TestAuth(unittest.TestCase):
|
|||||||
@mock.patch("blinkpy.auth.Auth.refresh_token")
|
@mock.patch("blinkpy.auth.Auth.refresh_token")
|
||||||
def test_query_retry_failed(self, mock_refresh, mock_validate):
|
def test_query_retry_failed(self, mock_refresh, mock_validate):
|
||||||
"""Check handling of failed retry request."""
|
"""Check handling of failed retry request."""
|
||||||
self.auth.seession = MockSession()
|
self.auth.session = MockSession()
|
||||||
mock_validate.side_effect = [UnauthorizedError, BlinkBadResponse]
|
mock_validate.side_effect = [UnauthorizedError, BlinkBadResponse]
|
||||||
mock_refresh.return_value = True
|
mock_refresh.return_value = True
|
||||||
self.assertEqual(self.auth.query(url="http://example.com"), None)
|
self.assertEqual(self.auth.query(url="http://example.com"), None)
|
||||||
@@ -254,6 +245,49 @@ class TestAuth(unittest.TestCase):
|
|||||||
mock_validate.side_effect = [UnauthorizedError, TokenRefreshFailed]
|
mock_validate.side_effect = [UnauthorizedError, TokenRefreshFailed]
|
||||||
self.assertEqual(self.auth.query(url="http://example.com"), None)
|
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:
|
class MockSession:
|
||||||
"""Object to mock a session."""
|
"""Object to mock a session."""
|
||||||
|
|||||||
@@ -2,32 +2,27 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
from blinkpy import blinkpy
|
from blinkpy import blinkpy
|
||||||
from blinkpy.sync_module import BlinkSyncModule
|
from blinkpy.sync_module import BlinkSyncModule
|
||||||
|
from blinkpy.camera import BlinkCamera
|
||||||
from blinkpy.helpers.util import get_time, BlinkURLHandler
|
from blinkpy.helpers.util import get_time, BlinkURLHandler
|
||||||
|
|
||||||
|
|
||||||
class MockSyncModule(BlinkSyncModule):
|
class MockSyncModule(BlinkSyncModule):
|
||||||
"""Mock http requests from sync module."""
|
"""Mock blink sync module object."""
|
||||||
|
|
||||||
def __init__(self, blink, header):
|
def get_network_info(self):
|
||||||
"""Create mock sync module instance."""
|
"""Mock network info method."""
|
||||||
super().__init__(blink, header, network_id=None, camera_list=None)
|
return True
|
||||||
self.blink = blink
|
|
||||||
self.header = header
|
|
||||||
self.return_value = None
|
|
||||||
self.return_value2 = None
|
|
||||||
|
|
||||||
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):
|
class MockCamera(BlinkCamera):
|
||||||
"""Mock post request."""
|
"""Mock blink camera object."""
|
||||||
return self.return_value
|
|
||||||
|
def update(self, config, force_cache=False, **kwargs):
|
||||||
|
"""Mock camera update method."""
|
||||||
|
|
||||||
|
|
||||||
class TestBlinkFunctions(unittest.TestCase):
|
class TestBlinkFunctions(unittest.TestCase):
|
||||||
@@ -94,9 +89,33 @@ class TestBlinkFunctions(unittest.TestCase):
|
|||||||
"DEBUG:blinkpy.blinkpy:foo: /bar.mp4 is marked as deleted.",
|
"DEBUG:blinkpy.blinkpy:foo: /bar.mp4 is marked as deleted.",
|
||||||
]
|
]
|
||||||
with self.assertLogs() as dl_log:
|
with self.assertLogs() as dl_log:
|
||||||
blink.download_videos("/tmp", stop=2)
|
blink.download_videos("/tmp", stop=2, delay=0)
|
||||||
self.assertEqual(dl_log.output, expected_log)
|
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")
|
@mock.patch("blinkpy.blinkpy.api.request_videos")
|
||||||
def test_parse_camera_not_in_list(self, mock_req):
|
def test_parse_camera_not_in_list(self, mock_req):
|
||||||
"""Test ability to parse downloaded items list."""
|
"""Test ability to parse downloaded items list."""
|
||||||
@@ -119,5 +138,18 @@ class TestBlinkFunctions(unittest.TestCase):
|
|||||||
"DEBUG:blinkpy.blinkpy:Skipping videos for foo.",
|
"DEBUG:blinkpy.blinkpy:Skipping videos for foo.",
|
||||||
]
|
]
|
||||||
with self.assertLogs() as dl_log:
|
with self.assertLogs() as dl_log:
|
||||||
blink.download_videos("/tmp", camera="bar", stop=2)
|
blink.download_videos("/tmp", camera="bar", stop=2, delay=0)
|
||||||
self.assertEqual(dl_log.output, expected_log)
|
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())
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ class TestBlinkSetup(unittest.TestCase):
|
|||||||
self.assertEqual(self.blink.last_refresh, None)
|
self.assertEqual(self.blink.last_refresh, None)
|
||||||
with mock.patch(
|
with mock.patch(
|
||||||
"blinkpy.sync_module.BlinkSyncModule.refresh", return_value=True
|
"blinkpy.sync_module.BlinkSyncModule.refresh", return_value=True
|
||||||
):
|
), mock.patch("blinkpy.blinkpy.Blink.get_homescreen", return_value=True):
|
||||||
self.blink.refresh()
|
self.blink.refresh(force=True)
|
||||||
|
|
||||||
self.assertEqual(self.blink.last_refresh, now)
|
self.assertEqual(self.blink.last_refresh, now)
|
||||||
self.assertEqual(self.blink.check_if_ok_to_update(), False)
|
self.assertEqual(self.blink.check_if_ok_to_update(), False)
|
||||||
|
|||||||
@@ -117,10 +117,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
|||||||
"WARNING:blinkpy.camera:Could not retrieve calibrated "
|
"WARNING:blinkpy.camera:Could not retrieve calibrated "
|
||||||
"temperature."
|
"temperature."
|
||||||
),
|
),
|
||||||
(
|
("WARNING:blinkpy.camera:Could not find thumbnail for camera new"),
|
||||||
"WARNING:blinkpy.camera:Could not find thumbnail for camera new"
|
|
||||||
"\nNoneType: None"
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -179,3 +176,10 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
|||||||
attr = camera.attributes
|
attr = camera.attributes
|
||||||
for key in attr:
|
for key in attr:
|
||||||
self.assertEqual(attr[key], None)
|
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")
|
||||||
|
|||||||
+17
-1
@@ -3,7 +3,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
import time
|
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):
|
class TestUtil(unittest.TestCase):
|
||||||
@@ -132,3 +132,19 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(json_load("fake.file"), None)
|
self.assertEqual(json_load("fake.file"), None)
|
||||||
with mock.patch("builtins.open", mock.mock_open(read_data="")):
|
with mock.patch("builtins.open", mock.mock_open(read_data="")):
|
||||||
self.assertEqual(json_load("fake.file"), None)
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user