Compare commits
174
Commits
v0.11.1
...
v0.14.3.dev0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ed2e3dab5 | ||
|
|
2b9c82b432 | ||
|
|
3e7c9f3d5c | ||
|
|
8d0b51ed9c | ||
|
|
c178fa5457 | ||
|
|
532ad92714 | ||
|
|
deb0a682d8 | ||
|
|
12e22dee8e | ||
|
|
ae0f954632 | ||
|
|
b92941538b | ||
|
|
ce13444427 | ||
|
|
9c461c4cfa | ||
|
|
23858c2912 | ||
|
|
2284a56c82 | ||
|
|
323143cb2b | ||
|
|
d6064a9aac | ||
|
|
a982aa595f | ||
|
|
54c52a7f1e | ||
|
|
bdb8cedf64 | ||
|
|
554b18adfe | ||
|
|
5619af2a00 | ||
|
|
957003aeb1 | ||
|
|
6fc64a486e | ||
|
|
534e3fb34e | ||
|
|
9ab346ed91 | ||
|
|
1bf5dda803 | ||
|
|
6cd8fd8ab2 | ||
|
|
a689839616 | ||
|
|
783aa5e547 | ||
|
|
a6ff2fa0df | ||
|
|
eb498bfb22 | ||
|
|
2ebbe46ada | ||
|
|
2f4e8fcade | ||
|
|
efe05525e4 | ||
|
|
4122ae8f64 | ||
|
|
5d296cc836 | ||
|
|
fa50e315a9 | ||
|
|
4a4811bee2 | ||
|
|
ad7ece4d45 | ||
|
|
17d1aa9690 | ||
|
|
b00839ecb3 | ||
|
|
4900ea8ba6 | ||
|
|
4fb16398e4 | ||
|
|
a1fc0c5a2b | ||
|
|
6adb252cdd | ||
|
|
3637bbad8d | ||
|
|
694efe71f8 | ||
|
|
315735bc2f | ||
|
|
db6403b468 | ||
|
|
92dda40e84 | ||
|
|
6c8fc0abce | ||
|
|
2d74b12d43 | ||
|
|
197695ee40 | ||
|
|
490c42550d | ||
|
|
7367eb6e87 | ||
|
|
414b647dc8 | ||
|
|
57882bb811 | ||
|
|
bb76f09f31 | ||
|
|
741cf489c8 | ||
|
|
7ce332b3ca | ||
|
|
8b7a491c89 | ||
|
|
604eebd1b7 | ||
|
|
c2f44b3dfd | ||
|
|
f4480da276 | ||
|
|
ccad3dcb0c | ||
|
|
7767d3af65 | ||
|
|
79e699ba78 | ||
|
|
b28d6f4eb4 | ||
|
|
d021f48c67 | ||
|
|
1e71af28dd | ||
|
|
cb5867d430 | ||
|
|
bb887936b6 | ||
|
|
720d520398 | ||
|
|
8fb1116641 | ||
|
|
c319762152 | ||
|
|
726d8b8c65 | ||
|
|
683650e2c2 | ||
|
|
50b1a35168 | ||
|
|
b88a5feddf | ||
|
|
93472e38de | ||
|
|
85c14ede8d | ||
|
|
58ce109518 | ||
|
|
cf65648293 | ||
|
|
d46e7ed96d | ||
|
|
00395b3825 | ||
|
|
f67f0dd6a2 | ||
|
|
d90cd3309f | ||
|
|
130aa8382a | ||
|
|
e836a6fa58 | ||
|
|
92d89b9fe5 | ||
|
|
c10fb432f7 | ||
|
|
37b729b597 | ||
|
|
749c68bb8a | ||
|
|
dcf0ce6394 | ||
|
|
102190e61b | ||
|
|
a7340c97ca | ||
|
|
43c1162634 | ||
|
|
4db7a33ef3 | ||
|
|
bfdc1e47bd | ||
|
|
afdd73118d | ||
|
|
5511af6244 | ||
|
|
d629d9fa3a | ||
|
|
3e22d83962 | ||
|
|
57b05daad7 | ||
|
|
419eb51b66 | ||
|
|
0262f14a53 | ||
|
|
c4b255d3bf | ||
|
|
13b7debb33 | ||
|
|
a203c4a29f | ||
|
|
d8bac39c52 | ||
|
|
f1c06198b2 | ||
|
|
95b45f507a | ||
|
|
54bfb99d60 | ||
|
|
9dda477e3d | ||
|
|
2ca6b9edda | ||
|
|
b22764614d | ||
|
|
09c168f6e8 | ||
|
|
414eabd3ae | ||
|
|
b536bd7eb5 | ||
|
|
532a8d5071 | ||
|
|
793797e3a7 | ||
|
|
602b6160ab | ||
|
|
aff95ba9e9 | ||
|
|
74aad2af1c | ||
|
|
b96ad8b4e4 | ||
|
|
a8b731fe21 | ||
|
|
ff8f091a74 | ||
|
|
147b6143b0 | ||
|
|
2835ccf7d2 | ||
|
|
4deb53eab8 | ||
|
|
1c49b1a943 | ||
|
|
67cd4ab962 | ||
|
|
1a2017b337 | ||
|
|
7775898078 | ||
|
|
8f0bd1224d | ||
|
|
77e200f6a5 | ||
|
|
d880998c43 | ||
|
|
fbd82030bb | ||
|
|
162b409c52 | ||
|
|
8e2d2e81e8 | ||
|
|
5504dfa94e | ||
|
|
d5f866bd34 | ||
|
|
02937c0ef5 | ||
|
|
ae5d73e197 | ||
|
|
15da35a6bc | ||
|
|
9738f84457 | ||
|
|
663a128469 | ||
|
|
bc14ab053c | ||
|
|
3b897f6dbc | ||
|
|
92f908928d | ||
|
|
da537773a7 | ||
|
|
778592c6cf | ||
|
|
0e33457ed0 | ||
|
|
abcf2ae1a1 | ||
|
|
28fb53029c | ||
|
|
d809cbcb3d | ||
|
|
8dea1a4312 | ||
|
|
45f7dd6cbb | ||
|
|
ac28dddd8b | ||
|
|
aa58383318 | ||
|
|
b0698f246a | ||
|
|
46c5d5eb3c | ||
|
|
1922b2535c | ||
|
|
4297290fba | ||
|
|
1daf5d22af | ||
|
|
8fd989e991 | ||
|
|
559cc4a5ac | ||
|
|
66a66c9bd3 | ||
|
|
f3a8036649 | ||
|
|
5c6e4364e8 | ||
|
|
fd64f41b22 | ||
|
|
0d7aac3458 | ||
|
|
0bebcd8303 | ||
|
|
ff35c56c9a |
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
[run]
|
[run]
|
||||||
omit =
|
parallel = true
|
||||||
helpers/*
|
omit =
|
||||||
tests/*
|
tests/*
|
||||||
setup.py
|
setup.py
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
name: coverage
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, dev ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master, dev ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
strategy:
|
||||||
|
max-parallel: 4
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- ubuntu-latest
|
||||||
|
python-version: [3.8]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install -r requirements_test.txt
|
||||||
|
pip install tox
|
||||||
|
pip install codecov
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
tox -r -e cov
|
||||||
|
- name: Codecov
|
||||||
|
uses: codecov/codecov-action@v1.0.6
|
||||||
|
with:
|
||||||
|
flags: unittests
|
||||||
|
file: ./coverage.xml
|
||||||
|
name: blinkpy
|
||||||
|
fail_ci_if_error: true
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||||
|
|
||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, dev ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master, dev ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.8]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install -r requirements_test.txt
|
||||||
|
pip install tox
|
||||||
|
- name: Lint
|
||||||
|
run: |
|
||||||
|
tox -r -e lint
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# This workflows will upload a Python Package using Twine when a release is created
|
||||||
|
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
|
||||||
|
|
||||||
|
name: Upload Python Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: '3.7'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install setuptools wheel twine
|
||||||
|
- name: Build and publish
|
||||||
|
env:
|
||||||
|
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||||
|
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
python setup.py bdist_wheel
|
||||||
|
twine upload dist/*
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, dev ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master, dev ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
strategy:
|
||||||
|
max-parallel: 4
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- ubuntu-latest
|
||||||
|
python-version: [3.5, 3.6, 3.7, 3.8]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install -r requirements_test.txt
|
||||||
|
pip install tox
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
tox -r
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
__pycache__/*
|
__pycache__/*
|
||||||
htmlcov/*
|
htmlcov/*
|
||||||
.coverage
|
.coverage
|
||||||
|
coverage.xml
|
||||||
*.pyc
|
*.pyc
|
||||||
*.egg*/*
|
*.egg*/*
|
||||||
dist/*
|
dist/*
|
||||||
|
|||||||
@@ -4,4 +4,8 @@ python:
|
|||||||
ignored_directories:
|
ignored_directories:
|
||||||
- docs
|
- docs
|
||||||
|
|
||||||
|
flake8:
|
||||||
|
enabled: true
|
||||||
|
config_file: .flake8
|
||||||
|
|
||||||
fail_on_violations: true
|
fail_on_violations: true
|
||||||
|
|||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
sudo: required
|
|
||||||
matrix:
|
|
||||||
fast_finish: true
|
|
||||||
include:
|
|
||||||
- python: "3.6"
|
|
||||||
env: TOXENV=lint
|
|
||||||
- python: "3.5.3"
|
|
||||||
env: TOXENV=py35
|
|
||||||
- python: "3.6"
|
|
||||||
env: TOXENV=py36
|
|
||||||
- python: "3.6"
|
|
||||||
env: TOXENV=build
|
|
||||||
- python: "3.7"
|
|
||||||
env: TOXENV=py37
|
|
||||||
dist: xenial
|
|
||||||
- python: "3.8-dev"
|
|
||||||
env: TOXENV=py38
|
|
||||||
dist: xenial
|
|
||||||
|
|
||||||
install: pip install -U tox coveralls
|
|
||||||
language: python
|
|
||||||
script: tox
|
|
||||||
after_success: coveralls
|
|
||||||
+81
-2
@@ -3,6 +3,85 @@ Changelog
|
|||||||
|
|
||||||
A list of changes between each release
|
A list of changes between each release
|
||||||
|
|
||||||
|
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.
|
||||||
|
- Previous logic for calculating percentage was incorrect
|
||||||
|
- raw battery voltage can be accessed via ``BlinkCamera.battery_voltage``
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
|
||||||
|
- Updated video endpoint (fixes broken motion detection)
|
||||||
|
- Removed throttling from critical api methods which prevented proper operation of multi-sync unit setups
|
||||||
|
- Slugify downloaded video names to allow for OS interoperability
|
||||||
|
- Added one minute offset (``Blink.motion_interval``) when checking for recent motion to allow time for events to propagate to server prior to refresh call.
|
||||||
|
|
||||||
|
**Everything else:**
|
||||||
|
|
||||||
|
- Changed all urls to use ``rest-region`` rather than ``rest.region``. Ability to revert to old method is enabled by instantiating ``Blink()`` with the ``legacy_subdomain`` variable set to ``True``.
|
||||||
|
- Added debug mode to ``blinkpy.download_videos`` routine to simply print the videos prepped for download, rather than actually saving them.
|
||||||
|
- Use UTC for time conversions, rather than local timezone
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
- Adds throttle decorator
|
||||||
|
- Decorate following functions with 4s throttle (call method with ``force=True`` to override):
|
||||||
|
- request_network_status
|
||||||
|
- request_syncmodule
|
||||||
|
- request_system_arm
|
||||||
|
- request_system_disarm
|
||||||
|
- request_sync_events
|
||||||
|
- request_new_image
|
||||||
|
- request_new_video
|
||||||
|
- request_video_count
|
||||||
|
- request_cameras
|
||||||
|
- request_camera_info
|
||||||
|
- request_camera_sensors
|
||||||
|
- request_motion_detection_enable
|
||||||
|
- request_motion_detection_disable
|
||||||
|
- Use the updated homescreen api endpoint to retrieve camera information. The old method to retrieve all cameras at once seems to not exist, and this was the only solution I could figure out and confirm to work.
|
||||||
|
- Adds throttle decorator to refresh function to prevent too many frequent calls with ``force_cache`` flag set to ``True``. This additional throttle can be overridden with the ``force=True`` argument passed to the refresh function.
|
||||||
|
- Add ability to cycle through login api endpoints to anticipate future endpoint deprecation
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
- Prevent blinkpy from failing at setup due to api error
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
0.11.1 (2019-01-02)
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
- Fixed incorrect backup login url
|
- Fixed incorrect backup login url
|
||||||
@@ -24,7 +103,7 @@ A list of changes between each release
|
|||||||
0.10.2 (2018-10-30)
|
0.10.2 (2018-10-30)
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
- Set minimum required version of the requests library to 2.20.0 due to vulnerability in earlier releases.
|
- 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'
|
- When multiple networks detected, changed log level to ``warning`` from ``error``
|
||||||
|
|
||||||
|
|
||||||
0.10.1 (2018-10-18)
|
0.10.1 (2018-10-18)
|
||||||
@@ -52,7 +131,7 @@ A list of changes between each release
|
|||||||
- Use session for http requests
|
- Use session for http requests
|
||||||
|
|
||||||
**Breaking change:**
|
**Breaking change:**
|
||||||
- Cameras now accessed through sync module Blink.sync.cameras
|
- Cameras now accessed through sync module ``Blink.sync.cameras``
|
||||||
|
|
||||||
|
|
||||||
0.8.1 (2018-09-24)
|
0.8.1 (2018-09-24)
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
FROM python:3.7-alpine
|
||||||
|
LABEL maintainer="Kevin Fronczak <kfronczak@gmail.com>"
|
||||||
|
|
||||||
|
VOLUME /media
|
||||||
|
|
||||||
|
RUN python -m pip install --upgrade pip
|
||||||
|
RUN pip3 install blinkpy
|
||||||
|
|
||||||
|
COPY app/ .
|
||||||
|
|
||||||
|
ENTRYPOINT ["python", "./app.py"]
|
||||||
|
CMD []
|
||||||
+39
-4
@@ -2,6 +2,10 @@ blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version| |Python Version|
|
|||||||
================================================================================
|
================================================================================
|
||||||
A Python library for the Blink Camera system
|
A Python library for the Blink Camera system
|
||||||
|
|
||||||
|
Like the library? Consider buying me a cup of coffee!
|
||||||
|
|
||||||
|
|Donate|
|
||||||
|
|
||||||
Disclaimer:
|
Disclaimer:
|
||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
Published under the MIT license - See LICENSE file for more details.
|
Published under the MIT license - See LICENSE file for more details.
|
||||||
@@ -51,6 +55,23 @@ The simplest way to use this package from a terminal is to call ``Blink.start()`
|
|||||||
|
|
||||||
If you would like to log in without setting up the cameras or system, you can simply call the ``Blink.login()`` function which will prompt for a username and password and then authenticate with the server. This is useful if you want to avoid use of the ``start()`` function which simply acts as a wrapper for more targeted API methods.
|
If you would like to log in without setting up the cameras or system, you can simply call the ``Blink.login()`` function which will prompt for a username and password and then authenticate with the server. This is useful if you want to avoid use of the ``start()`` function which simply acts as a wrapper for more targeted API methods.
|
||||||
|
|
||||||
|
In addition, you can also save your credentials in a json file and initialize Blink with the credential file as follows:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from blinkpy import blinkpy
|
||||||
|
blink = blinkpy.Blink(cred_file="path/to/credentials.json")
|
||||||
|
blink.start()
|
||||||
|
|
||||||
|
The credential file must be json formatted with a ``username`` and ``password`` key like follows:
|
||||||
|
|
||||||
|
.. code:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
'username': 'YOUR USER NAME',
|
||||||
|
'password': 'YOUR PASSWORD'
|
||||||
|
}
|
||||||
|
|
||||||
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).
|
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:
|
The below code will display cameras and their available attributes:
|
||||||
@@ -84,14 +105,28 @@ The ``blinkpy`` api also allows for saving images and videos to a file and snapp
|
|||||||
blink.refresh() # Get new information from server
|
blink.refresh() # Get new information from server
|
||||||
camera.image_to_file('/local/path/for/image.jpg')
|
camera.image_to_file('/local/path/for/image.jpg')
|
||||||
camera.video_to_file('/local/path/for/video.mp4')
|
camera.video_to_file('/local/path/for/video.mp4')
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
.. |Build Status| image:: https://travis-ci.org/fronzbot/blinkpy.svg?branch=dev
|
Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34am to the ``/home/blink`` directory:
|
||||||
:target: https://travis-ci.org/fronzbot/blinkpy
|
|
||||||
.. |Coverage Status| image:: https://coveralls.io/repos/github/fronzbot/blinkpy/badge.svg?branch=dev
|
.. code:: python
|
||||||
:target: https://coveralls.io/github/fronzbot/blinkpy?branch=dev
|
|
||||||
|
blink = blinkpy.Blink(username="YOUR USER NAME", password="YOUR PASSWORD")
|
||||||
|
blink.start()
|
||||||
|
blink.download_videos('/home/blink', since='2018/07/04 09:34')
|
||||||
|
|
||||||
|
|
||||||
|
.. |Build Status| image:: https://github.com/fronzbot/blinkpy/workflows/build/badge.svg
|
||||||
|
:target: https://github.com/fronzbot/blinkpy/actions?query=workflow%3Abuild
|
||||||
|
.. |Coverage Status| image:: https://codecov.io/gh/fronzbot/blinkpy/branch/dev/graph/badge.svg
|
||||||
|
:target: https://codecov.io/gh/fronzbot/blinkpy
|
||||||
.. |PyPi Version| image:: https://img.shields.io/pypi/v/blinkpy.svg
|
.. |PyPi Version| image:: https://img.shields.io/pypi/v/blinkpy.svg
|
||||||
:target: https://pypi.python.org/pypi/blinkpy
|
:target: https://pypi.python.org/pypi/blinkpy
|
||||||
.. |Docs| image:: https://readthedocs.org/projects/blinkpy/badge/?version=latest
|
.. |Docs| image:: https://readthedocs.org/projects/blinkpy/badge/?version=latest
|
||||||
:target: http://blinkpy.readthedocs.io/en/latest/?badge=latest
|
:target: http://blinkpy.readthedocs.io/en/latest/?badge=latest
|
||||||
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/blinkpy.svg
|
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/blinkpy.svg
|
||||||
:target: https://img.shields.io/pypi/pyversions/blinkpy.svg
|
:target: https://img.shields.io/pypi/pyversions/blinkpy.svg
|
||||||
|
|
||||||
|
.. |Donate| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif
|
||||||
|
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UR6Z2B8GXYUCC
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Python init file for app.py."""
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
"""Script to run blinkpy as an app."""
|
||||||
|
from os import environ
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from blinkpy import blinkpy
|
||||||
|
|
||||||
|
|
||||||
|
USERNAME = environ.get('USERNAME')
|
||||||
|
PASSWORD = environ.get('PASSWORD')
|
||||||
|
TIMEDELTA = timedelta(environ.get('TIMEDELTA', 1))
|
||||||
|
|
||||||
|
|
||||||
|
def get_date():
|
||||||
|
"""Return now - timedelta for blinkpy."""
|
||||||
|
return (datetime.now() - TIMEDELTA).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def download_videos(blink, save_dir='/media'):
|
||||||
|
"""Make request to download videos."""
|
||||||
|
blink.download_videos(save_dir, since=get_date())
|
||||||
|
|
||||||
|
|
||||||
|
def start():
|
||||||
|
"""Startup blink app."""
|
||||||
|
blink = blinkpy.Blink(username=USERNAME, password=PASSWORD)
|
||||||
|
blink.start()
|
||||||
|
return blink
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
BLINK = start()
|
||||||
|
download_videos(BLINK)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
docker build -t fronzbot/blinkpy:latest ./
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# bash run.sh [username] [password]
|
||||||
|
|
||||||
|
if [ "$#" -ne 2 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "ERROR: Requries Blink username and password as arguments."
|
||||||
|
echo "bash run.sh [username] [password]"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
USER=fronzbot
|
||||||
|
IMAGE=blinkpy
|
||||||
|
CONFIG=$HOME/blinkpy_media
|
||||||
|
USERNAME=$1
|
||||||
|
PASSWORD=$2
|
||||||
|
|
||||||
|
mkdir -p $CONFIG
|
||||||
|
|
||||||
|
result=$(docker images -q $IMAGE)
|
||||||
|
if [ $result ]; then
|
||||||
|
docker rm $IMAGE
|
||||||
|
fi
|
||||||
|
docker run -it --name ${IMAGE} \
|
||||||
|
-v $CONFIG:/media \
|
||||||
|
-e USERNAME=${USERNAME} \
|
||||||
|
-e PASSWORD=${PASSWORD} \
|
||||||
|
$USER/$IMAGE \
|
||||||
|
/bin/bash
|
||||||
|
|
||||||
+132
-29
@@ -3,14 +3,24 @@
|
|||||||
import logging
|
import logging
|
||||||
from json import dumps
|
from json import dumps
|
||||||
import blinkpy.helpers.errors as ERROR
|
import blinkpy.helpers.errors as ERROR
|
||||||
from blinkpy.helpers.util import http_req, BlinkException
|
from blinkpy.helpers.util import http_req, get_time, BlinkException, Throttle
|
||||||
from blinkpy.helpers.constants import DEFAULT_URL
|
from blinkpy.helpers.constants import DEFAULT_URL
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MIN_THROTTLE_TIME = 2
|
||||||
|
|
||||||
def request_login(blink, url, username, password):
|
|
||||||
"""Login request."""
|
def request_login(blink, url, username, password, is_retry=False):
|
||||||
|
"""
|
||||||
|
Login request.
|
||||||
|
|
||||||
|
:param blink: Blink instance.
|
||||||
|
:param url: Login url.
|
||||||
|
:param username: Blink username.
|
||||||
|
:param password: Blink password.
|
||||||
|
:param is_retry: Is this part of a re-authorization attempt?
|
||||||
|
"""
|
||||||
headers = {
|
headers = {
|
||||||
'Host': DEFAULT_URL,
|
'Host': DEFAULT_URL,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -21,7 +31,7 @@ def request_login(blink, url, username, password):
|
|||||||
'client_specifier': 'iPhone 9.2 | 2.2 | 222'
|
'client_specifier': 'iPhone 9.2 | 2.2 | 222'
|
||||||
})
|
})
|
||||||
return http_req(blink, url=url, headers=headers, data=data,
|
return http_req(blink, url=url, headers=headers, data=data,
|
||||||
json_resp=False, reqtype='post')
|
json_resp=False, reqtype='post', is_retry=is_retry)
|
||||||
|
|
||||||
|
|
||||||
def request_networks(blink):
|
def request_networks(blink):
|
||||||
@@ -31,137 +41,230 @@ def request_networks(blink):
|
|||||||
|
|
||||||
|
|
||||||
def request_network_status(blink, network):
|
def request_network_status(blink, network):
|
||||||
"""Request network information."""
|
"""
|
||||||
|
Request network information.
|
||||||
|
|
||||||
|
:param blink: Blink instance.
|
||||||
|
:param network: Sync module network id.
|
||||||
|
"""
|
||||||
url = "{}/network/{}".format(blink.urls.base_url, network)
|
url = "{}/network/{}".format(blink.urls.base_url, network)
|
||||||
return http_get(blink, url)
|
return http_get(blink, url)
|
||||||
|
|
||||||
|
|
||||||
def request_syncmodule(blink, network):
|
def request_syncmodule(blink, network):
|
||||||
"""Request sync module info."""
|
"""
|
||||||
|
Request sync module info.
|
||||||
|
|
||||||
|
:param blink: Blink instance.
|
||||||
|
:param network: Sync module network id.
|
||||||
|
"""
|
||||||
url = "{}/network/{}/syncmodules".format(blink.urls.base_url, network)
|
url = "{}/network/{}/syncmodules".format(blink.urls.base_url, network)
|
||||||
return http_get(blink, url)
|
return http_get(blink, url)
|
||||||
|
|
||||||
|
|
||||||
|
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||||
def request_system_arm(blink, network):
|
def request_system_arm(blink, network):
|
||||||
"""Arm system."""
|
"""
|
||||||
|
Arm system.
|
||||||
|
|
||||||
|
:param blink: Blink instance.
|
||||||
|
:param network: Sync module network id.
|
||||||
|
"""
|
||||||
url = "{}/network/{}/arm".format(blink.urls.base_url, network)
|
url = "{}/network/{}/arm".format(blink.urls.base_url, network)
|
||||||
return http_post(blink, url)
|
return http_post(blink, url)
|
||||||
|
|
||||||
|
|
||||||
|
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||||
def request_system_disarm(blink, network):
|
def request_system_disarm(blink, network):
|
||||||
"""Disarm system."""
|
"""
|
||||||
|
Disarm system.
|
||||||
|
|
||||||
|
:param blink: Blink instance.
|
||||||
|
:param network: Sync module network id.
|
||||||
|
"""
|
||||||
url = "{}/network/{}/disarm".format(blink.urls.base_url, network)
|
url = "{}/network/{}/disarm".format(blink.urls.base_url, network)
|
||||||
return http_post(blink, url)
|
return http_post(blink, url)
|
||||||
|
|
||||||
|
|
||||||
def request_command_status(blink, network, command_id):
|
def request_command_status(blink, network, command_id):
|
||||||
"""Request command status."""
|
"""
|
||||||
|
Request command status.
|
||||||
|
|
||||||
|
:param blink: Blink instance.
|
||||||
|
:param network: Sync module network id.
|
||||||
|
:param command_id: Command id to check.
|
||||||
|
"""
|
||||||
url = "{}/network/{}/command/{}".format(blink.urls.base_url,
|
url = "{}/network/{}/command/{}".format(blink.urls.base_url,
|
||||||
network,
|
network,
|
||||||
command_id)
|
command_id)
|
||||||
return http_get(blink, url)
|
return http_get(blink, url)
|
||||||
|
|
||||||
|
|
||||||
|
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||||
def request_homescreen(blink):
|
def request_homescreen(blink):
|
||||||
"""Request homescreen info."""
|
"""Request homescreen info."""
|
||||||
url = "{}/homescreen".format(blink.urls.base_url)
|
url = "{}/api/v3/accounts/{}/homescreen".format(blink.urls.base_url,
|
||||||
|
blink.account_id)
|
||||||
return http_get(blink, url)
|
return http_get(blink, url)
|
||||||
|
|
||||||
|
|
||||||
|
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||||
def request_sync_events(blink, network):
|
def request_sync_events(blink, network):
|
||||||
"""Request events from sync module."""
|
"""
|
||||||
|
Request events from sync module.
|
||||||
|
|
||||||
|
:param blink: Blink instance.
|
||||||
|
:param network: Sync module network id.
|
||||||
|
"""
|
||||||
url = "{}/events/network/{}".format(blink.urls.base_url, network)
|
url = "{}/events/network/{}".format(blink.urls.base_url, network)
|
||||||
return http_get(blink, url)
|
return http_get(blink, url)
|
||||||
|
|
||||||
|
|
||||||
|
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||||
def request_new_image(blink, network, camera_id):
|
def request_new_image(blink, network, camera_id):
|
||||||
"""Request to capture new thumbnail for camera."""
|
"""
|
||||||
|
Request to capture new thumbnail for camera.
|
||||||
|
|
||||||
|
:param blink: Blink instance.
|
||||||
|
: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,
|
url = "{}/network/{}/camera/{}/thumbnail".format(blink.urls.base_url,
|
||||||
network,
|
network,
|
||||||
camera_id)
|
camera_id)
|
||||||
return http_post(blink, url)
|
return http_post(blink, url)
|
||||||
|
|
||||||
|
|
||||||
|
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||||
def request_new_video(blink, network, camera_id):
|
def request_new_video(blink, network, camera_id):
|
||||||
"""Request to capture new video clip."""
|
"""
|
||||||
|
Request to capture new video clip.
|
||||||
|
|
||||||
|
:param blink: Blink instance.
|
||||||
|
: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,
|
url = "{}/network/{}/camera/{}/clip".format(blink.urls.base_url,
|
||||||
network,
|
network,
|
||||||
camera_id)
|
camera_id)
|
||||||
return http_post(blink, url)
|
return http_post(blink, url)
|
||||||
|
|
||||||
|
|
||||||
def request_video_count(blink, headers):
|
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||||
|
def request_video_count(blink):
|
||||||
"""Request total video count."""
|
"""Request total video count."""
|
||||||
url = "{}/api/v2/videos/count".format(blink.urls.base_url)
|
url = "{}/api/v2/videos/count".format(blink.urls.base_url)
|
||||||
return http_get(blink, url)
|
return http_get(blink, url)
|
||||||
|
|
||||||
|
|
||||||
def request_videos(blink, page=0):
|
def request_videos(blink, time=None, page=0):
|
||||||
"""Perform a request for videos."""
|
"""
|
||||||
url = "{}/api/v2/videos/page/{}".format(blink.urls.base_url, page)
|
Perform a request for videos.
|
||||||
|
|
||||||
|
:param blink: Blink instance.
|
||||||
|
:param time: Get videos since this time. In epoch seconds.
|
||||||
|
: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)
|
||||||
return http_get(blink, url)
|
return http_get(blink, url)
|
||||||
|
|
||||||
|
|
||||||
def request_cameras(blink, network):
|
def request_cameras(blink, network):
|
||||||
"""Request all camera information."""
|
"""
|
||||||
|
Request all camera information.
|
||||||
|
|
||||||
|
:param Blink: Blink instance.
|
||||||
|
:param network: Sync module network id.
|
||||||
|
"""
|
||||||
url = "{}/network/{}/cameras".format(blink.urls.base_url, network)
|
url = "{}/network/{}/cameras".format(blink.urls.base_url, network)
|
||||||
return http_get(blink, url)
|
return http_get(blink, url)
|
||||||
|
|
||||||
|
|
||||||
def request_camera_info(blink, network, camera_id):
|
def request_camera_info(blink, network, camera_id):
|
||||||
"""Request camera info for one camera."""
|
"""
|
||||||
url = "{}/network/{}/camera/{}".format(blink.urls.base_url,
|
Request camera info for one camera.
|
||||||
network,
|
|
||||||
camera_id)
|
:param blink: Blink instance.
|
||||||
|
: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)
|
||||||
return http_get(blink, url)
|
return http_get(blink, url)
|
||||||
|
|
||||||
|
|
||||||
def request_camera_sensors(blink, network, camera_id):
|
def request_camera_sensors(blink, network, camera_id):
|
||||||
"""Request camera sensor info for one camera."""
|
"""
|
||||||
|
Request camera sensor info for one camera.
|
||||||
|
|
||||||
|
:param blink: Blink instance.
|
||||||
|
: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,
|
url = "{}/network/{}/camera/{}/signals".format(blink.urls.base_url,
|
||||||
network,
|
network,
|
||||||
camera_id)
|
camera_id)
|
||||||
return http_get(blink, url)
|
return http_get(blink, url)
|
||||||
|
|
||||||
|
|
||||||
|
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||||
def request_motion_detection_enable(blink, network, camera_id):
|
def request_motion_detection_enable(blink, network, camera_id):
|
||||||
"""Enable motion detection for a camera."""
|
"""
|
||||||
|
Enable motion detection for a camera.
|
||||||
|
|
||||||
|
:param blink: Blink instance.
|
||||||
|
:param network: Sync module network id.
|
||||||
|
:param camera_id: Camera ID of camera to enable.
|
||||||
|
"""
|
||||||
url = "{}/network/{}/camera/{}/enable".format(blink.urls.base_url,
|
url = "{}/network/{}/camera/{}/enable".format(blink.urls.base_url,
|
||||||
network,
|
network,
|
||||||
camera_id)
|
camera_id)
|
||||||
return http_post(blink, url)
|
return http_post(blink, url)
|
||||||
|
|
||||||
|
|
||||||
|
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||||
def request_motion_detection_disable(blink, network, camera_id):
|
def request_motion_detection_disable(blink, network, camera_id):
|
||||||
"""Disable motion detection for a camera."""
|
"""Disable motion detection for a camera.
|
||||||
|
|
||||||
|
:param blink: Blink instance.
|
||||||
|
:param network: Sync module network id.
|
||||||
|
:param camera_id: Camera ID of camera to disable.
|
||||||
|
"""
|
||||||
url = "{}/network/{}/camera/{}/disable".format(blink.urls.base_url,
|
url = "{}/network/{}/camera/{}/disable".format(blink.urls.base_url,
|
||||||
network,
|
network,
|
||||||
camera_id)
|
camera_id)
|
||||||
return http_post(blink, url)
|
return http_post(blink, url)
|
||||||
|
|
||||||
|
|
||||||
def http_get(blink, url, stream=False, json=True):
|
def http_get(blink, url, stream=False, json=True, is_retry=False):
|
||||||
"""
|
"""
|
||||||
Perform an http get request.
|
Perform an http get request.
|
||||||
|
|
||||||
:param url: URL to perform get request.
|
:param url: URL to perform get request.
|
||||||
:param stream: Stream response? True/FALSE
|
:param stream: Stream response? True/FALSE
|
||||||
:param json: Return json response? TRUE/False
|
:param json: Return json response? TRUE/False
|
||||||
|
:param is_retry: Is this part of a re-auth attempt?
|
||||||
"""
|
"""
|
||||||
if blink.auth_header is None:
|
if blink.auth_header is None:
|
||||||
raise BlinkException(ERROR.AUTH_TOKEN)
|
raise BlinkException(ERROR.AUTH_TOKEN)
|
||||||
_LOGGER.debug("Making GET request to %s", url)
|
_LOGGER.debug("Making GET request to %s", url)
|
||||||
return http_req(blink, url=url, headers=blink.auth_header,
|
return http_req(blink, url=url, headers=blink.auth_header,
|
||||||
reqtype='get', stream=stream, json_resp=json)
|
reqtype='get', stream=stream, json_resp=json,
|
||||||
|
is_retry=is_retry)
|
||||||
|
|
||||||
|
|
||||||
def http_post(blink, url):
|
def http_post(blink, url, is_retry=False):
|
||||||
"""
|
"""
|
||||||
Perform an http post request.
|
Perform an http post request.
|
||||||
|
|
||||||
:param url: URL to perfom post request.
|
:param url: URL to perfom post request.
|
||||||
|
:param is_retry: Is this part of a re-auth attempt?
|
||||||
"""
|
"""
|
||||||
if blink.auth_header is None:
|
if blink.auth_header is None:
|
||||||
raise BlinkException(ERROR.AUTH_TOKEN)
|
raise BlinkException(ERROR.AUTH_TOKEN)
|
||||||
_LOGGER.debug("Making POST request to %s", url)
|
_LOGGER.debug("Making POST request to %s", url)
|
||||||
return http_req(blink, url=url, headers=blink.auth_header, reqtype='post')
|
return http_req(blink, url=url, headers=blink.auth_header,
|
||||||
|
reqtype='post', is_retry=is_retry)
|
||||||
|
|||||||
+224
-47
@@ -1,49 +1,74 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
blinkpy by Kevin Fronczak - A Blink camera Python library.
|
blinkpy is an unofficial api for the Blink security camera system.
|
||||||
|
|
||||||
|
repo url: https://github.com/fronzbot/blinkpy
|
||||||
|
|
||||||
https://github.com/fronzbot/blinkpy
|
|
||||||
Original protocol hacking by MattTW :
|
Original protocol hacking by MattTW :
|
||||||
https://github.com/MattTW/BlinkMonitorProtocol
|
https://github.com/MattTW/BlinkMonitorProtocol
|
||||||
|
|
||||||
Published under the MIT license - See LICENSE file for more details.
|
Published under the MIT license - See LICENSE file for more details.
|
||||||
"Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark
|
"Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark
|
||||||
owned by Immedia Inc., see www.blinkforhome.com for more information.
|
owned by Immedia Inc., see www.blinkforhome.com for more information.
|
||||||
I am in no way affiliated with Blink, nor Immedia Inc.
|
blinkpy is in no way affiliated with Blink, nor Immedia Inc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os.path
|
||||||
import time
|
import time
|
||||||
import getpass
|
import getpass
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
|
from shutil import copyfileobj
|
||||||
|
|
||||||
from requests.structures import CaseInsensitiveDict
|
from requests.structures import CaseInsensitiveDict
|
||||||
import blinkpy.helpers.errors as ERROR
|
from dateutil.parser import parse
|
||||||
|
from slugify import slugify
|
||||||
|
|
||||||
from blinkpy import api
|
from blinkpy import api
|
||||||
from blinkpy.sync_module import BlinkSyncModule
|
from blinkpy.sync_module import BlinkSyncModule
|
||||||
|
from blinkpy.helpers import errors as ERROR
|
||||||
from blinkpy.helpers.util import (
|
from blinkpy.helpers.util import (
|
||||||
create_session, merge_dicts, BlinkURLHandler,
|
create_session, merge_dicts, get_time, BlinkURLHandler,
|
||||||
BlinkAuthenticationException)
|
BlinkAuthenticationException, Throttle)
|
||||||
from blinkpy.helpers.constants import (
|
from blinkpy.helpers.constants import (
|
||||||
BLINK_URL, LOGIN_URL, LOGIN_BACKUP_URL)
|
BLINK_URL, LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL,
|
||||||
|
DEFAULT_MOTION_INTERVAL, DEFAULT_REFRESH, MIN_THROTTLE_TIME)
|
||||||
|
from blinkpy.helpers.constants import __version__
|
||||||
|
|
||||||
REFRESH_RATE = 30
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger('blinkpy')
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Blink():
|
class Blink():
|
||||||
"""Class to initialize communication."""
|
"""Class to initialize communication."""
|
||||||
|
|
||||||
def __init__(self, username=None, password=None,
|
def __init__(self, username=None, password=None,
|
||||||
refresh_rate=REFRESH_RATE):
|
cred_file=None,
|
||||||
|
refresh_rate=DEFAULT_REFRESH,
|
||||||
|
motion_interval=DEFAULT_MOTION_INTERVAL,
|
||||||
|
legacy_subdomain=False):
|
||||||
"""
|
"""
|
||||||
Initialize Blink system.
|
Initialize Blink system.
|
||||||
|
|
||||||
:param username: Blink username (usually email address)
|
:param username: Blink username (usually email address)
|
||||||
:param password: Blink password
|
:param password: Blink password
|
||||||
|
:param cred_file: JSON formatted file to store credentials.
|
||||||
|
If username and password are given, file
|
||||||
|
is ignored. Otherwise, username and password
|
||||||
|
are loaded from file.
|
||||||
:param refresh_rate: Refresh rate of blink information.
|
:param refresh_rate: Refresh rate of blink information.
|
||||||
Defaults to 15 (seconds)
|
Defaults to 15 (seconds)
|
||||||
|
:param motion_interval: How far back to register motion in minutes.
|
||||||
|
Defaults to last refresh time.
|
||||||
|
Useful for preventing motion_detected property
|
||||||
|
from de-asserting too quickly.
|
||||||
|
:param legacy_subdomain: Set to TRUE to use old 'rest.region'
|
||||||
|
endpoints (only use if you are having
|
||||||
|
api issues).
|
||||||
"""
|
"""
|
||||||
self._username = username
|
self._username = username
|
||||||
self._password = password
|
self._password = password
|
||||||
|
self._cred_file = cred_file
|
||||||
self._token = None
|
self._token = None
|
||||||
self._auth_header = None
|
self._auth_header = None
|
||||||
self._host = None
|
self._host = None
|
||||||
@@ -55,10 +80,15 @@ class Blink():
|
|||||||
self.region_id = None
|
self.region_id = None
|
||||||
self.last_refresh = None
|
self.last_refresh = None
|
||||||
self.refresh_rate = refresh_rate
|
self.refresh_rate = refresh_rate
|
||||||
self.session = None
|
self.session = create_session()
|
||||||
self.networks = []
|
self.networks = []
|
||||||
self.cameras = CaseInsensitiveDict({})
|
self.cameras = CaseInsensitiveDict({})
|
||||||
|
self.video_list = CaseInsensitiveDict({})
|
||||||
self._login_url = LOGIN_URL
|
self._login_url = LOGIN_URL
|
||||||
|
self.login_urls = []
|
||||||
|
self.motion_interval = motion_interval
|
||||||
|
self.version = __version__
|
||||||
|
self.legacy = legacy_subdomain
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auth_header(self):
|
def auth_header(self):
|
||||||
@@ -73,73 +103,113 @@ class Blink():
|
|||||||
Essentially this is just a wrapper function for ease of use.
|
Essentially this is just a wrapper function for ease of use.
|
||||||
"""
|
"""
|
||||||
if self._username is None or self._password is None:
|
if self._username is None or self._password is None:
|
||||||
self.login()
|
if not self.login():
|
||||||
else:
|
return
|
||||||
self.get_auth_token()
|
elif not self.get_auth_token():
|
||||||
|
return
|
||||||
|
|
||||||
|
camera_list = self.get_cameras()
|
||||||
networks = self.get_ids()
|
networks = self.get_ids()
|
||||||
for network_name, network_id in networks.items():
|
for network_name, network_id in networks.items():
|
||||||
sync_module = BlinkSyncModule(self, network_name, network_id)
|
if network_id not in camera_list.keys():
|
||||||
|
camera_list[network_id] = {}
|
||||||
|
_LOGGER.warning("No cameras found for %s", network_name)
|
||||||
|
sync_module = BlinkSyncModule(self,
|
||||||
|
network_name,
|
||||||
|
network_id,
|
||||||
|
camera_list[network_id])
|
||||||
sync_module.start()
|
sync_module.start()
|
||||||
self.sync[network_name] = sync_module
|
self.sync[network_name] = sync_module
|
||||||
self.cameras = self.merge_cameras()
|
self.cameras = self.merge_cameras()
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
"""Prompt user for username and password."""
|
"""Prompt user for username and password."""
|
||||||
self._username = input("Username:")
|
if self._cred_file is not None and os.path.isfile(self._cred_file):
|
||||||
self._password = getpass.getpass("Password:")
|
try:
|
||||||
|
with open(self._cred_file, 'r') as json_file:
|
||||||
|
creds = json.load(json_file)
|
||||||
|
self._username = creds['username']
|
||||||
|
self._password = creds['password']
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.error("Improperly formated json file %s.",
|
||||||
|
self._cred_file,
|
||||||
|
exc_info=True)
|
||||||
|
return False
|
||||||
|
except KeyError:
|
||||||
|
_LOGGER.error("JSON file information incomplete %s.",
|
||||||
|
exc_info=True)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self._username = input("Username:")
|
||||||
|
self._password = getpass.getpass("Password:")
|
||||||
|
|
||||||
if self.get_auth_token():
|
if self.get_auth_token():
|
||||||
_LOGGER.info("Login successful!")
|
_LOGGER.debug("Login successful!")
|
||||||
return True
|
return True
|
||||||
_LOGGER.warning("Unable to login with %s.", self._username)
|
_LOGGER.warning("Unable to login with %s.", self._username)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_auth_token(self):
|
def get_auth_token(self, is_retry=False):
|
||||||
"""Retrieve the authentication token from Blink."""
|
"""Retrieve the authentication token from Blink."""
|
||||||
if not isinstance(self._username, str):
|
if not isinstance(self._username, str):
|
||||||
raise BlinkAuthenticationException(ERROR.USERNAME)
|
raise BlinkAuthenticationException(ERROR.USERNAME)
|
||||||
if not isinstance(self._password, str):
|
if not isinstance(self._password, str):
|
||||||
raise BlinkAuthenticationException(ERROR.PASSWORD)
|
raise BlinkAuthenticationException(ERROR.PASSWORD)
|
||||||
|
|
||||||
login_url = LOGIN_URL
|
self.login_urls = [LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL]
|
||||||
self.session = create_session()
|
|
||||||
response = api.request_login(self,
|
|
||||||
login_url,
|
|
||||||
self._username,
|
|
||||||
self._password)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
response = self.login_request(is_retry=is_retry)
|
||||||
response = response.json()
|
|
||||||
(self.region_id, self.region), = response['region'].items()
|
if not response:
|
||||||
else:
|
return False
|
||||||
_LOGGER.debug(
|
|
||||||
("Received response code %s "
|
|
||||||
"when authenticating, "
|
|
||||||
"trying new url"), response.status_code
|
|
||||||
)
|
|
||||||
login_url = LOGIN_BACKUP_URL
|
|
||||||
response = api.request_login(self,
|
|
||||||
login_url,
|
|
||||||
self._username,
|
|
||||||
self._password)
|
|
||||||
self.region_id = 'piri'
|
|
||||||
self.region = "UNKNOWN"
|
|
||||||
|
|
||||||
self._host = "{}.{}".format(self.region_id, BLINK_URL)
|
self._host = "{}.{}".format(self.region_id, BLINK_URL)
|
||||||
self._token = response['authtoken']['authtoken']
|
self._token = response['authtoken']['authtoken']
|
||||||
|
self.networks = response['networks']
|
||||||
|
|
||||||
self._auth_header = {'Host': self._host,
|
self._auth_header = {'Host': self._host,
|
||||||
'TOKEN_AUTH': self._token}
|
'TOKEN_AUTH': self._token}
|
||||||
self.networks = response['networks']
|
self.urls = BlinkURLHandler(self.region_id, legacy=self.legacy)
|
||||||
self.urls = BlinkURLHandler(self.region_id)
|
|
||||||
self._login_url = login_url
|
|
||||||
|
|
||||||
return self._auth_header
|
return self._auth_header
|
||||||
|
|
||||||
|
def login_request(self, is_retry=False):
|
||||||
|
"""Make a login request."""
|
||||||
|
try:
|
||||||
|
login_url = self.login_urls.pop(0)
|
||||||
|
except IndexError:
|
||||||
|
_LOGGER.error("Could not login to blink servers.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
_LOGGER.info("Attempting login with %s", login_url)
|
||||||
|
|
||||||
|
response = api.request_login(self,
|
||||||
|
login_url,
|
||||||
|
self._username,
|
||||||
|
self._password,
|
||||||
|
is_retry=is_retry)
|
||||||
|
try:
|
||||||
|
if response.status_code != 200:
|
||||||
|
response = self.login_request(is_retry=True)
|
||||||
|
response = response.json()
|
||||||
|
(self.region_id, self.region), = response['region'].items()
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
_LOGGER.error("Login API endpoint failed with response %s",
|
||||||
|
response)
|
||||||
|
return False
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
_LOGGER.warning("Could not extract region info.")
|
||||||
|
self.region_id = 'piri'
|
||||||
|
self.region = 'UNKNOWN'
|
||||||
|
|
||||||
|
self._login_url = login_url
|
||||||
|
return response
|
||||||
|
|
||||||
def get_ids(self):
|
def get_ids(self):
|
||||||
"""Set the network ID and Account ID."""
|
"""Set the network ID and Account ID."""
|
||||||
response = api.request_networks(self)
|
response = api.request_networks(self)
|
||||||
# Look for only onboarded network, flag warning if multiple
|
|
||||||
# since it's unexpected
|
|
||||||
all_networks = []
|
all_networks = []
|
||||||
network_dict = {}
|
network_dict = {}
|
||||||
for network, status in self.networks.items():
|
for network, status in self.networks.items():
|
||||||
@@ -156,6 +226,26 @@ class Blink():
|
|||||||
self.network_ids = all_networks
|
self.network_ids = all_networks
|
||||||
return network_dict
|
return network_dict
|
||||||
|
|
||||||
|
def get_cameras(self):
|
||||||
|
"""Retrieve a camera list for each onboarded network."""
|
||||||
|
response = api.request_homescreen(self)
|
||||||
|
try:
|
||||||
|
all_cameras = {}
|
||||||
|
for camera in response['cameras']:
|
||||||
|
camera_network = str(camera['network_id'])
|
||||||
|
camera_name = camera['name']
|
||||||
|
camera_id = camera['id']
|
||||||
|
camera_info = {'name': camera_name, 'id': camera_id}
|
||||||
|
if camera_network not in all_cameras:
|
||||||
|
all_cameras[camera_network] = []
|
||||||
|
|
||||||
|
all_cameras[camera_network].append(camera_info)
|
||||||
|
return all_cameras
|
||||||
|
except KeyError:
|
||||||
|
_LOGGER.error("Initialization failue. Could not retrieve cameras.")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||||
def refresh(self, force_cache=False):
|
def refresh(self, force_cache=False):
|
||||||
"""
|
"""
|
||||||
Perform a system refresh.
|
Perform a system refresh.
|
||||||
@@ -166,6 +256,11 @@ class Blink():
|
|||||||
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_cache)
|
sync_module.refresh(force_cache=force_cache)
|
||||||
|
if not force_cache:
|
||||||
|
# Prevents rapid clearing of motion detect property
|
||||||
|
self.last_refresh = int(time.time())
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def check_if_ok_to_update(self):
|
def check_if_ok_to_update(self):
|
||||||
"""Check if it is ok to perform an http request."""
|
"""Check if it is ok to perform an http request."""
|
||||||
@@ -174,7 +269,6 @@ class Blink():
|
|||||||
if last_refresh is None:
|
if last_refresh is None:
|
||||||
last_refresh = 0
|
last_refresh = 0
|
||||||
if current_time >= (last_refresh + self.refresh_rate):
|
if current_time >= (last_refresh + self.refresh_rate):
|
||||||
self.last_refresh = current_time
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -184,3 +278,86 @@ class Blink():
|
|||||||
for sync in self.sync:
|
for sync in self.sync:
|
||||||
combined = merge_dicts(combined, self.sync[sync].cameras)
|
combined = merge_dicts(combined, self.sync[sync].cameras)
|
||||||
return combined
|
return combined
|
||||||
|
|
||||||
|
def download_videos(self, path, since=None,
|
||||||
|
camera='all', stop=10, 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
|
||||||
|
: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 debug: Set to TRUE to prevent downloading of items.
|
||||||
|
Instead of downloading, entries will be printed to log.
|
||||||
|
"""
|
||||||
|
if since is None:
|
||||||
|
since_epochs = self.last_refresh
|
||||||
|
else:
|
||||||
|
parsed_datetime = parse(since, fuzzy=True)
|
||||||
|
since_epochs = parsed_datetime.timestamp()
|
||||||
|
|
||||||
|
formatted_date = get_time(time_to_convert=since_epochs)
|
||||||
|
_LOGGER.info("Retrieving videos since %s", formatted_date)
|
||||||
|
|
||||||
|
if not isinstance(camera, list):
|
||||||
|
camera = [camera]
|
||||||
|
|
||||||
|
for page in range(1, stop):
|
||||||
|
response = api.request_videos(self, time=since_epochs, page=page)
|
||||||
|
_LOGGER.debug("Processing page %s", page)
|
||||||
|
try:
|
||||||
|
result = response['media']
|
||||||
|
if not result:
|
||||||
|
raise IndexError
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
_LOGGER.info("No videos found on page %s. Exiting.", page)
|
||||||
|
break
|
||||||
|
|
||||||
|
self._parse_downloaded_items(result, camera, path, debug)
|
||||||
|
|
||||||
|
def _parse_downloaded_items(self, result, camera, path, debug):
|
||||||
|
"""Parse downloaded videos."""
|
||||||
|
for item in result:
|
||||||
|
try:
|
||||||
|
created_at = item['created_at']
|
||||||
|
camera_name = item['device_name']
|
||||||
|
is_deleted = item['deleted']
|
||||||
|
address = item['media']
|
||||||
|
except KeyError:
|
||||||
|
_LOGGER.info("Missing clip information, skipping...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if camera_name not in camera and 'all' not in camera:
|
||||||
|
_LOGGER.debug("Skipping videos for %s.", camera_name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if is_deleted:
|
||||||
|
_LOGGER.debug("%s: %s is marked as deleted.",
|
||||||
|
camera_name,
|
||||||
|
address)
|
||||||
|
continue
|
||||||
|
|
||||||
|
clip_address = "{}{}".format(self.urls.base_url, address)
|
||||||
|
filename = "{}-{}".format(camera_name, created_at)
|
||||||
|
filename = "{}.mp4".format(slugify(filename))
|
||||||
|
filename = os.path.join(path, filename)
|
||||||
|
|
||||||
|
if not debug:
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
_LOGGER.info("%s already exists, skipping...", filename)
|
||||||
|
continue
|
||||||
|
|
||||||
|
response = api.http_get(self, url=clip_address,
|
||||||
|
stream=True, json=False)
|
||||||
|
with open(filename, 'wb') as vidfile:
|
||||||
|
copyfileobj(response.raw, vidfile)
|
||||||
|
|
||||||
|
_LOGGER.info("Downloaded video to %s", filename)
|
||||||
|
else:
|
||||||
|
print(("Camera: {}, Timestamp: {}, "
|
||||||
|
"Address: {}, Filename: {}").format(
|
||||||
|
camera_name, created_at, address, filename))
|
||||||
|
|||||||
+24
-49
@@ -6,8 +6,6 @@ from blinkpy import api
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
MAX_CLIPS = 5
|
|
||||||
|
|
||||||
|
|
||||||
class BlinkCamera():
|
class BlinkCamera():
|
||||||
"""Class to initialize individual camera."""
|
"""Class to initialize individual camera."""
|
||||||
@@ -28,7 +26,7 @@ class BlinkCamera():
|
|||||||
self.battery_state = None
|
self.battery_state = None
|
||||||
self.motion_detected = None
|
self.motion_detected = None
|
||||||
self.wifi_strength = None
|
self.wifi_strength = None
|
||||||
self.last_record = []
|
self.last_record = None
|
||||||
self._cached_image = None
|
self._cached_image = None
|
||||||
self._cached_video = None
|
self._cached_video = None
|
||||||
|
|
||||||
@@ -43,6 +41,7 @@ class BlinkCamera():
|
|||||||
'temperature_c': self.temperature_c,
|
'temperature_c': self.temperature_c,
|
||||||
'temperature_calibrated': self.temperature_calibrated,
|
'temperature_calibrated': self.temperature_calibrated,
|
||||||
'battery': self.battery,
|
'battery': self.battery,
|
||||||
|
'battery_voltage': self.battery_voltage,
|
||||||
'thumbnail': self.thumbnail,
|
'thumbnail': self.thumbnail,
|
||||||
'video': self.clip,
|
'video': self.clip,
|
||||||
'motion_enabled': self.motion_enabled,
|
'motion_enabled': self.motion_enabled,
|
||||||
@@ -56,8 +55,8 @@ class BlinkCamera():
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def battery(self):
|
def battery(self):
|
||||||
"""Return battery level as percentage."""
|
"""Return battery as string."""
|
||||||
return round(self.battery_voltage / 180 * 100)
|
return self.battery_state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def temperature_c(self):
|
def temperature_c(self):
|
||||||
@@ -94,10 +93,11 @@ class BlinkCamera():
|
|||||||
self.network_id,
|
self.network_id,
|
||||||
self.camera_id)
|
self.camera_id)
|
||||||
|
|
||||||
def update(self, config, force_cache=False):
|
def update(self, config, force_cache=False, **kwargs):
|
||||||
"""Update camera info."""
|
"""Update camera info."""
|
||||||
|
# force = kwargs.pop('force', False)
|
||||||
self.name = config['name']
|
self.name = config['name']
|
||||||
self.camera_id = str(config['camera_id'])
|
self.camera_id = str(config['id'])
|
||||||
self.network_id = str(config['network_id'])
|
self.network_id = str(config['network_id'])
|
||||||
self.serial = config['serial']
|
self.serial = config['serial']
|
||||||
self.motion_enabled = config['enabled']
|
self.motion_enabled = config['enabled']
|
||||||
@@ -113,27 +113,34 @@ class BlinkCamera():
|
|||||||
try:
|
try:
|
||||||
self.temperature_calibrated = resp['temp']
|
self.temperature_calibrated = resp['temp']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
_LOGGER.error("Could not retrieve calibrated temperature.")
|
self.temperature_calibrated = self.temperature
|
||||||
|
_LOGGER.warning("Could not retrieve calibrated temperature.")
|
||||||
|
|
||||||
# Check if thumbnail exists in config, if not try to
|
# Check if thumbnail exists in config, if not try to
|
||||||
# get it from the homescreen info in teh sync module
|
# get it from the homescreen info in the sync module
|
||||||
# otherwise set it to None and log an error
|
# otherwise set it to None and log an error
|
||||||
new_thumbnail = None
|
new_thumbnail = None
|
||||||
|
thumb_addr = None
|
||||||
if config['thumbnail']:
|
if config['thumbnail']:
|
||||||
thumb_addr = config['thumbnail']
|
thumb_addr = config['thumbnail']
|
||||||
else:
|
else:
|
||||||
thumb_addr = self.get_thumb_from_homescreen()
|
_LOGGER.warning("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 = "{}{}.jpg".format(self.sync.urls.base_url,
|
new_thumbnail = "{}{}.jpg".format(self.sync.urls.base_url,
|
||||||
thumb_addr)
|
thumb_addr)
|
||||||
|
|
||||||
# Check if a new motion clip has been recorded
|
try:
|
||||||
# check_for_motion_method sets motion_detected variable
|
self.motion_detected = self.sync.motion[self.name]
|
||||||
self.check_for_motion()
|
except KeyError:
|
||||||
|
self.motion_detected = False
|
||||||
|
|
||||||
clip_addr = None
|
clip_addr = None
|
||||||
if self.last_record:
|
if self.name in self.sync.last_record:
|
||||||
clip_addr = self.sync.all_clips[self.name][self.last_record[0]]
|
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,
|
self.clip = "{}{}".format(self.sync.urls.base_url,
|
||||||
clip_addr)
|
clip_addr)
|
||||||
|
|
||||||
@@ -158,25 +165,6 @@ class BlinkCamera():
|
|||||||
stream=True,
|
stream=True,
|
||||||
json=False)
|
json=False)
|
||||||
|
|
||||||
def check_for_motion(self):
|
|
||||||
"""Check if motion detected.."""
|
|
||||||
try:
|
|
||||||
records = sorted(self.sync.record_dates[self.name])
|
|
||||||
new_clip = records.pop()
|
|
||||||
if new_clip not in self.last_record and self.last_record:
|
|
||||||
self.motion_detected = True
|
|
||||||
self.last_record.insert(0, new_clip)
|
|
||||||
if len(self.last_record) > MAX_CLIPS:
|
|
||||||
self.last_record.pop()
|
|
||||||
elif not self.last_record:
|
|
||||||
self.last_record.insert(0, new_clip)
|
|
||||||
self.motion_detected = False
|
|
||||||
else:
|
|
||||||
self.motion_detected = False
|
|
||||||
except KeyError:
|
|
||||||
_LOGGER.info("Could not extract clip info from camera %s",
|
|
||||||
self.name)
|
|
||||||
|
|
||||||
def image_to_file(self, path):
|
def image_to_file(self, path):
|
||||||
"""
|
"""
|
||||||
Write image to file.
|
Write image to file.
|
||||||
@@ -200,21 +188,8 @@ class BlinkCamera():
|
|||||||
_LOGGER.debug("Writing video from %s to %s", self.name, path)
|
_LOGGER.debug("Writing video from %s to %s", self.name, path)
|
||||||
response = self._cached_video
|
response = self._cached_video
|
||||||
if response is None:
|
if response is None:
|
||||||
_LOGGER.error("No saved video exist for %s.", self.name)
|
_LOGGER.error("No saved video exist for %s.",
|
||||||
|
self.name)
|
||||||
return
|
return
|
||||||
with open(path, 'wb') as vidfile:
|
with open(path, 'wb') as vidfile:
|
||||||
copyfileobj(response.raw, vidfile)
|
copyfileobj(response.raw, vidfile)
|
||||||
|
|
||||||
def get_thumb_from_homescreen(self):
|
|
||||||
"""Retrieve thumbnail from homescreen."""
|
|
||||||
for device in self.sync.homescreen['devices']:
|
|
||||||
try:
|
|
||||||
device_type = device['device_type']
|
|
||||||
device_name = device['name']
|
|
||||||
device_thumb = device['thumbnail']
|
|
||||||
if device_type == 'camera' and device_name == self.name:
|
|
||||||
return device_thumb
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
_LOGGER.error("Could not find thumbnail for camera %s", self.name)
|
|
||||||
return None
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
MAJOR_VERSION = 0
|
MAJOR_VERSION = 0
|
||||||
MINOR_VERSION = 11
|
MINOR_VERSION = 14
|
||||||
PATCH_VERSION = 1
|
PATCH_VERSION = '3.dev0'
|
||||||
|
|
||||||
__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
|
__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ PROJECT_CLASSIFIERS = [
|
|||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.5',
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3.6',
|
||||||
'Programming Language :: Python :: 3.7',
|
'Programming Language :: Python :: 3.7',
|
||||||
|
'Programming Language :: Python :: 3.8',
|
||||||
'Topic :: Home Automation'
|
'Topic :: Home Automation'
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -45,12 +46,22 @@ PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME)
|
|||||||
URLS
|
URLS
|
||||||
'''
|
'''
|
||||||
BLINK_URL = 'immedia-semi.com'
|
BLINK_URL = 'immedia-semi.com'
|
||||||
DEFAULT_URL = "{}.{}".format('prod', BLINK_URL)
|
DEFAULT_URL = "{}.{}".format('rest-prod', BLINK_URL)
|
||||||
BASE_URL = "https://{}".format(DEFAULT_URL)
|
BASE_URL = "https://{}".format(DEFAULT_URL)
|
||||||
LOGIN_URL = "{}/login".format(BASE_URL)
|
LOGIN_URL = "{}/api/v2/login".format(BASE_URL)
|
||||||
LOGIN_BACKUP_URL = "https://{}.{}/login".format('rest.piri', BLINK_URL)
|
OLD_LOGIN_URL = "{}/login".format(BASE_URL)
|
||||||
|
LOGIN_BACKUP_URL = "https://{}.{}/login".format('rest-piri', BLINK_URL)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Dictionaries
|
Dictionaries
|
||||||
'''
|
'''
|
||||||
ONLINE = {'online': True, 'offline': False}
|
ONLINE = {'online': True, 'offline': False}
|
||||||
|
|
||||||
|
'''
|
||||||
|
OTHER
|
||||||
|
'''
|
||||||
|
TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S%z'
|
||||||
|
|
||||||
|
DEFAULT_MOTION_INTERVAL = 1
|
||||||
|
DEFAULT_REFRESH = 30
|
||||||
|
MIN_THROTTLE_TIME = 2
|
||||||
|
|||||||
+68
-7
@@ -1,14 +1,36 @@
|
|||||||
"""Useful functions for blinkpy."""
|
"""Useful functions for blinkpy."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
|
from calendar import timegm
|
||||||
|
from functools import partial, wraps
|
||||||
from requests import Request, Session, exceptions
|
from requests import Request, Session, exceptions
|
||||||
from blinkpy.helpers.constants import BLINK_URL
|
import dateutil.parser
|
||||||
|
from blinkpy.helpers.constants import BLINK_URL, TIMESTAMP_FORMAT
|
||||||
import blinkpy.helpers.errors as ERROR
|
import blinkpy.helpers.errors as ERROR
|
||||||
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def time_to_seconds(timestamp):
|
||||||
|
"""Convert TIMESTAMP_FORMAT time to seconds."""
|
||||||
|
try:
|
||||||
|
dtime = dateutil.parser.isoparse(timestamp)
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.error("Incorrect timestamp format for conversion: %s.",
|
||||||
|
timestamp)
|
||||||
|
return False
|
||||||
|
return timegm(dtime.timetuple())
|
||||||
|
|
||||||
|
|
||||||
|
def get_time(time_to_convert=None):
|
||||||
|
"""Create blink-compatible timestamp."""
|
||||||
|
if time_to_convert is None:
|
||||||
|
time_to_convert = time.time()
|
||||||
|
return time.strftime(TIMESTAMP_FORMAT, time.gmtime(time_to_convert))
|
||||||
|
|
||||||
|
|
||||||
def merge_dicts(dict_a, dict_b):
|
def merge_dicts(dict_a, dict_b):
|
||||||
"""Merge two dictionaries into one."""
|
"""Merge two dictionaries into one."""
|
||||||
duplicates = [val for val in dict_a if val in dict_b]
|
duplicates = [val for val in dict_a if val in dict_b]
|
||||||
@@ -19,15 +41,21 @@ def merge_dicts(dict_a, dict_b):
|
|||||||
|
|
||||||
|
|
||||||
def create_session():
|
def create_session():
|
||||||
"""Create a session for blink communication."""
|
"""
|
||||||
|
Create a session for blink communication.
|
||||||
|
|
||||||
|
From @ericfrederich via
|
||||||
|
https://github.com/kennethreitz/requests/issues/2011
|
||||||
|
"""
|
||||||
sess = Session()
|
sess = Session()
|
||||||
|
sess.get = partial(sess.get, timeout=5)
|
||||||
return sess
|
return sess
|
||||||
|
|
||||||
|
|
||||||
def attempt_reauthorization(blink):
|
def attempt_reauthorization(blink):
|
||||||
"""Attempt to refresh auth token and links."""
|
"""Attempt to refresh auth token and links."""
|
||||||
_LOGGER.info("Auth token expired, attempting reauthorization.")
|
_LOGGER.info("Auth token expired, attempting reauthorization.")
|
||||||
headers = blink.get_auth_token()
|
headers = blink.get_auth_token(is_retry=True)
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
|
||||||
@@ -63,6 +91,8 @@ def http_req(blink, url='http://example.com', data=None, headers=None,
|
|||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
headers = attempt_reauthorization(blink)
|
headers = attempt_reauthorization(blink)
|
||||||
|
if not headers:
|
||||||
|
raise exceptions.ConnectionError
|
||||||
return http_req(blink, url=url, data=data, headers=headers,
|
return http_req(blink, url=url, data=data, headers=headers,
|
||||||
reqtype=reqtype, stream=stream,
|
reqtype=reqtype, stream=stream,
|
||||||
json_resp=json_resp, is_retry=True)
|
json_resp=json_resp, is_retry=True)
|
||||||
@@ -96,18 +126,49 @@ class BlinkException(Exception):
|
|||||||
class BlinkAuthenticationException(BlinkException):
|
class BlinkAuthenticationException(BlinkException):
|
||||||
"""Class to throw authentication exception."""
|
"""Class to throw authentication exception."""
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class BlinkURLHandler():
|
class BlinkURLHandler():
|
||||||
"""Class that handles Blink URLS."""
|
"""Class that handles Blink URLS."""
|
||||||
|
|
||||||
def __init__(self, region_id):
|
def __init__(self, region_id, legacy=False):
|
||||||
"""Initialize the urls."""
|
"""Initialize the urls."""
|
||||||
self.base_url = "https://rest.{}.{}".format(region_id, BLINK_URL)
|
self.subdomain = 'rest-{}'.format(region_id)
|
||||||
|
if legacy:
|
||||||
|
self.subdomain = 'rest.{}'.format(region_id)
|
||||||
|
self.base_url = "https://{}.{}".format(self.subdomain, BLINK_URL)
|
||||||
self.home_url = "{}/homescreen".format(self.base_url)
|
self.home_url = "{}/homescreen".format(self.base_url)
|
||||||
self.event_url = "{}/events/network".format(self.base_url)
|
self.event_url = "{}/events/network".format(self.base_url)
|
||||||
self.network_url = "{}/network".format(self.base_url)
|
self.network_url = "{}/network".format(self.base_url)
|
||||||
self.networks_url = "{}/networks".format(self.base_url)
|
self.networks_url = "{}/networks".format(self.base_url)
|
||||||
self.video_url = "{}/api/v2/videos".format(self.base_url)
|
self.video_url = "{}/api/v2/videos".format(self.base_url)
|
||||||
_LOGGER.debug("Setting base url to %s.", self.base_url)
|
_LOGGER.debug("Setting base url to %s.", self.base_url)
|
||||||
|
|
||||||
|
|
||||||
|
class Throttle():
|
||||||
|
"""Class for throttling api calls."""
|
||||||
|
|
||||||
|
def __init__(self, seconds=10):
|
||||||
|
"""Initialize throttle class."""
|
||||||
|
self.throttle_time = seconds
|
||||||
|
self.last_call = 0
|
||||||
|
|
||||||
|
def __call__(self, method):
|
||||||
|
"""Throttle caller method."""
|
||||||
|
def throttle_method():
|
||||||
|
"""Call when method is throttled."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
@wraps(method)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
"""Wrap that checks for throttling."""
|
||||||
|
force = kwargs.pop('force', False)
|
||||||
|
now = int(time.time())
|
||||||
|
last_call_delta = now - self.last_call
|
||||||
|
if force or last_call_delta > self.throttle_time:
|
||||||
|
result = method(*args, *kwargs)
|
||||||
|
self.last_call = now
|
||||||
|
return result
|
||||||
|
|
||||||
|
return throttle_method()
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|||||||
+109
-86
@@ -5,6 +5,7 @@ import logging
|
|||||||
from requests.structures import CaseInsensitiveDict
|
from requests.structures import CaseInsensitiveDict
|
||||||
from blinkpy import api
|
from blinkpy import api
|
||||||
from blinkpy.camera import BlinkCamera
|
from blinkpy.camera import BlinkCamera
|
||||||
|
from blinkpy.helpers.util import time_to_seconds
|
||||||
from blinkpy.helpers.constants import ONLINE
|
from blinkpy.helpers.constants import ONLINE
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -13,7 +14,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class BlinkSyncModule():
|
class BlinkSyncModule():
|
||||||
"""Class to initialize sync module."""
|
"""Class to initialize sync module."""
|
||||||
|
|
||||||
def __init__(self, blink, network_name, network_id):
|
def __init__(self, blink, network_name, network_id, camera_list):
|
||||||
"""
|
"""
|
||||||
Initialize Blink sync module.
|
Initialize Blink sync module.
|
||||||
|
|
||||||
@@ -30,13 +31,13 @@ class BlinkSyncModule():
|
|||||||
self.sync_id = None
|
self.sync_id = None
|
||||||
self.host = None
|
self.host = None
|
||||||
self.summary = None
|
self.summary = None
|
||||||
self.homescreen = None
|
|
||||||
self.network_info = None
|
self.network_info = None
|
||||||
self.record_dates = {}
|
|
||||||
self.videos = {}
|
|
||||||
self.events = []
|
self.events = []
|
||||||
self.cameras = CaseInsensitiveDict({})
|
self.cameras = CaseInsensitiveDict({})
|
||||||
self.all_clips = {}
|
self.motion_interval = blink.motion_interval
|
||||||
|
self.motion = {}
|
||||||
|
self.last_record = {}
|
||||||
|
self.camera_list = camera_list
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attributes(self):
|
def attributes(self):
|
||||||
@@ -65,7 +66,10 @@ class BlinkSyncModule():
|
|||||||
@property
|
@property
|
||||||
def arm(self):
|
def arm(self):
|
||||||
"""Return status of sync module: armed/disarmed."""
|
"""Return status of sync module: armed/disarmed."""
|
||||||
return self.network_info['network']['armed']
|
try:
|
||||||
|
return self.network_info['network']['armed']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
@arm.setter
|
@arm.setter
|
||||||
def arm(self, value):
|
def arm(self, value):
|
||||||
@@ -77,102 +81,121 @@ class BlinkSyncModule():
|
|||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
"""Initialize the system."""
|
"""Initialize the system."""
|
||||||
response = api.request_syncmodule(self.blink, self.network_id)
|
response = api.request_syncmodule(self.blink,
|
||||||
self.summary = response['syncmodule']
|
self.network_id)
|
||||||
self.sync_id = self.summary['id']
|
try:
|
||||||
self.network_id = self.summary['network_id']
|
self.summary = response['syncmodule']
|
||||||
self.serial = self.summary['serial']
|
self.network_id = self.summary['network_id']
|
||||||
self.status = self.summary['status']
|
except (TypeError, KeyError):
|
||||||
|
_LOGGER.error(("Could not retrieve sync module information "
|
||||||
|
"with response: %s"), response, exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
self.events = self.get_events()
|
try:
|
||||||
|
self.sync_id = self.summary['id']
|
||||||
self.homescreen = api.request_homescreen(self.blink)
|
self.serial = self.summary['serial']
|
||||||
|
self.status = self.summary['status']
|
||||||
|
except KeyError:
|
||||||
|
_LOGGER.error("Could not extract some sync module info: %s",
|
||||||
|
response,
|
||||||
|
exc_info=True)
|
||||||
|
|
||||||
self.network_info = api.request_network_status(self.blink,
|
self.network_info = api.request_network_status(self.blink,
|
||||||
self.network_id)
|
self.network_id)
|
||||||
|
|
||||||
camera_info = self.get_camera_info()
|
self.check_new_videos()
|
||||||
for camera_config in camera_info:
|
try:
|
||||||
name = camera_config['name']
|
for camera_config in self.camera_list:
|
||||||
self.cameras[name] = BlinkCamera(self)
|
if 'name' not in camera_config:
|
||||||
|
break
|
||||||
|
name = camera_config['name']
|
||||||
|
self.cameras[name] = BlinkCamera(self)
|
||||||
|
self.motion[name] = False
|
||||||
|
camera_info = self.get_camera_info(camera_config['id'])
|
||||||
|
self.cameras[name].update(camera_info,
|
||||||
|
force_cache=True,
|
||||||
|
force=True)
|
||||||
|
except KeyError:
|
||||||
|
_LOGGER.error("Could not create cameras instances for %s",
|
||||||
|
self.name,
|
||||||
|
exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
self.videos = self.get_videos()
|
return True
|
||||||
for camera_config in camera_info:
|
|
||||||
name = camera_config['name']
|
|
||||||
if name in self.cameras:
|
|
||||||
self.cameras[name].update(camera_config, force_cache=True)
|
|
||||||
|
|
||||||
def get_events(self):
|
def get_events(self, **kwargs):
|
||||||
"""Retrieve events from server."""
|
"""Retrieve events from server."""
|
||||||
response = api.request_sync_events(self.blink, self.network_id)
|
force = kwargs.pop('force', False)
|
||||||
return response['event']
|
response = api.request_sync_events(self.blink,
|
||||||
|
self.network_id,
|
||||||
|
force=force)
|
||||||
|
try:
|
||||||
|
return response['event']
|
||||||
|
except (TypeError, KeyError):
|
||||||
|
_LOGGER.error("Could not extract events: %s",
|
||||||
|
response,
|
||||||
|
exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
def get_camera_info(self):
|
def get_camera_info(self, camera_id):
|
||||||
"""Retrieve camera information."""
|
"""Retrieve camera information."""
|
||||||
response = api.request_cameras(self.blink, self.network_id)
|
response = api.request_camera_info(self.blink,
|
||||||
return response['devicestatus']
|
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 []
|
||||||
|
|
||||||
def refresh(self, force_cache=False):
|
def refresh(self, force_cache=False):
|
||||||
"""Get all blink cameras and pulls their most recent status."""
|
"""Get all blink cameras and pulls their most recent status."""
|
||||||
self.events = self.get_events()
|
|
||||||
self.videos = self.get_videos()
|
|
||||||
self.homescreen = api.request_homescreen(self.blink)
|
|
||||||
self.network_info = api.request_network_status(self.blink,
|
self.network_info = api.request_network_status(self.blink,
|
||||||
self.network_id)
|
self.network_id)
|
||||||
camera_info = self.get_camera_info()
|
self.check_new_videos()
|
||||||
for camera_config in camera_info:
|
for camera_name in self.cameras.keys():
|
||||||
name = camera_config['name']
|
camera_id = self.cameras[camera_name].camera_id
|
||||||
self.cameras[name].update(camera_config, force_cache=force_cache)
|
camera_info = self.get_camera_info(camera_id)
|
||||||
|
self.cameras[camera_name].update(camera_info,
|
||||||
|
force_cache=force_cache)
|
||||||
|
|
||||||
def get_videos(self, start_page=0, end_page=1):
|
def check_new_videos(self):
|
||||||
"""
|
"""Check if new videos since last refresh."""
|
||||||
Retrieve last recorded videos per camera.
|
try:
|
||||||
|
interval = self.blink.last_refresh - self.motion_interval * 60
|
||||||
|
except TypeError:
|
||||||
|
# This is the first start, so refresh hasn't happened yet.
|
||||||
|
# No need to check for motion.
|
||||||
|
return False
|
||||||
|
|
||||||
:param start_page: Page to start reading from on blink servers
|
resp = api.request_videos(self.blink,
|
||||||
(defaults to 0)
|
time=interval,
|
||||||
:param end_page: Page to stop reading from (defaults to 1)
|
page=1)
|
||||||
"""
|
|
||||||
videos = list()
|
|
||||||
all_dates = dict()
|
|
||||||
|
|
||||||
for page_num in range(start_page, end_page + 1):
|
for camera in self.cameras.keys():
|
||||||
this_page = api.request_videos(self.blink, page=page_num)
|
self.motion[camera] = False
|
||||||
if not this_page:
|
|
||||||
break
|
|
||||||
videos.append(this_page)
|
|
||||||
_LOGGER.debug("Getting videos from page %s through %s",
|
|
||||||
start_page,
|
|
||||||
end_page)
|
|
||||||
for page in videos:
|
|
||||||
for entry in page:
|
|
||||||
camera_name = entry['camera_name']
|
|
||||||
clip_addr = entry['address']
|
|
||||||
thumb_addr = entry['thumbnail']
|
|
||||||
clip_date = clip_addr.split('_')[-6:]
|
|
||||||
clip_date = '_'.join(clip_date)
|
|
||||||
clip_date = clip_date.split('.')[0]
|
|
||||||
try:
|
|
||||||
self.all_clips[camera_name][clip_date] = clip_addr
|
|
||||||
except KeyError:
|
|
||||||
self.all_clips[camera_name] = {clip_date: clip_addr}
|
|
||||||
|
|
||||||
if camera_name not in all_dates:
|
try:
|
||||||
all_dates[camera_name] = list()
|
info = resp['media']
|
||||||
all_dates[camera_name].append(clip_date)
|
except (KeyError, TypeError):
|
||||||
try:
|
_LOGGER.warning("Could not check for motion. Response: %s", resp)
|
||||||
self.videos[camera_name].append(
|
return False
|
||||||
{
|
|
||||||
'clip': clip_addr,
|
for entry in info:
|
||||||
'thumb': thumb_addr,
|
try:
|
||||||
}
|
name = entry['device_name']
|
||||||
)
|
clip = entry['media']
|
||||||
except KeyError:
|
timestamp = entry['created_at']
|
||||||
self.videos[camera_name] = [
|
if self.check_new_video_time(timestamp):
|
||||||
{
|
self.motion[name] = True and self.arm
|
||||||
'clip': clip_addr,
|
self.last_record[name] = {'clip': clip, 'time': timestamp}
|
||||||
'thumb': thumb_addr,
|
except KeyError:
|
||||||
}
|
_LOGGER.debug("No new videos since last refresh.")
|
||||||
]
|
|
||||||
self.record_dates = all_dates
|
return True
|
||||||
_LOGGER.debug("Retrieved a total of %s records", len(all_dates))
|
|
||||||
return self.videos
|
def check_new_video_time(self, timestamp):
|
||||||
|
"""Check if video has timestamp since last refresh."""
|
||||||
|
return time_to_seconds(timestamp) > self.blink.last_refresh
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ reports=no
|
|||||||
# too-many-* - are not enforced for the sake of readability
|
# too-many-* - are not enforced for the sake of readability
|
||||||
# too-few-* - same as too-many-*
|
# too-few-* - same as too-many-*
|
||||||
# no-else-return - I don't see any reason to enforce this. both forms are readable
|
# no-else-return - I don't see any reason to enforce this. both forms are readable
|
||||||
|
# no-self-use - stupid and only annoying
|
||||||
|
# unexpected-keyword-arg - doesn't allow for use of **kwargs, which is dumb
|
||||||
|
|
||||||
disable=
|
disable=
|
||||||
locally-disabled,
|
locally-disabled,
|
||||||
@@ -23,3 +25,5 @@ disable=
|
|||||||
too-many-lines,
|
too-many-lines,
|
||||||
too-few-public-methods,
|
too-few-public-methods,
|
||||||
no-else-return,
|
no-else-return,
|
||||||
|
no-self-use,
|
||||||
|
unexpected-keyword-arg,
|
||||||
|
|||||||
+3
-1
@@ -1,2 +1,4 @@
|
|||||||
requests>=2.20.0
|
python-dateutil~=2.8.0
|
||||||
|
requests==2.22.0
|
||||||
|
python-slugify~=3.0.2
|
||||||
testtools==2.3.0
|
testtools==2.3.0
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
flake8==3.5.0
|
coverage==4.5.4
|
||||||
flake8-docstrings==1.3.0
|
flake8==3.7.9
|
||||||
pylint==2.1.1
|
flake8-docstrings==1.5.0
|
||||||
pydocstyle==2.1.1
|
pylint==2.4.4
|
||||||
pytest==3.7.1
|
pydocstyle==5.0.2
|
||||||
pytest-cov>=2.3.1
|
pytest==5.3.5
|
||||||
pytest-sugar>=0.9.0
|
pytest-cov==2.8.1
|
||||||
pytest-timeout>=1.0.0
|
pytest-sugar>=0.9.2
|
||||||
|
pytest-timeout>=1.3.3
|
||||||
restructuredtext-lint>=1.0.1
|
restructuredtext-lint>=1.0.1
|
||||||
pygments>=2.2.0
|
pygments>=2.2.0
|
||||||
|
|||||||
@@ -67,5 +67,3 @@ def mocked_session_send(*args, **kwargs):
|
|||||||
|
|
||||||
class MockURLHandler(BlinkURLHandler):
|
class MockURLHandler(BlinkURLHandler):
|
||||||
"""Mocks URL Handler in blinkpy module."""
|
"""Mocks URL Handler in blinkpy module."""
|
||||||
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
"""Tests camera and system functions."""
|
"""Tests camera and system functions."""
|
||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
import logging
|
||||||
from requests import Request
|
|
||||||
|
|
||||||
from blinkpy import blinkpy
|
from blinkpy import blinkpy
|
||||||
from blinkpy.sync_module import BlinkSyncModule
|
from blinkpy.sync_module import BlinkSyncModule
|
||||||
from blinkpy.helpers.util import create_session
|
from blinkpy.helpers.util import create_session, get_time
|
||||||
import tests.mock_responses as mresp
|
import tests.mock_responses as mresp
|
||||||
|
|
||||||
USERNAME = 'foobar'
|
USERNAME = 'foobar'
|
||||||
@@ -56,29 +55,114 @@ class TestBlinkFunctions(unittest.TestCase):
|
|||||||
"""Clean up after test."""
|
"""Clean up after test."""
|
||||||
self.blink = None
|
self.blink = None
|
||||||
|
|
||||||
@mock.patch('blinkpy.blinkpy.api.http_req')
|
@mock.patch('blinkpy.blinkpy.api.request_login')
|
||||||
def test_backup_url(self, req, mock_sess):
|
def test_backup_url(self, req, mock_sess):
|
||||||
"""Test backup login method."""
|
"""Test backup login method."""
|
||||||
fake_req = Request('POST', 'http://wrong.url').prepare()
|
json_resp = {
|
||||||
|
'authtoken': {'authtoken': 'foobar123'},
|
||||||
|
'networks': {'1234': {'name': 'foobar', 'onboarded': True}}
|
||||||
|
}
|
||||||
|
bad_req = mresp.MockResponse({}, 404)
|
||||||
|
new_req = mresp.MockResponse(json_resp, 200)
|
||||||
req.side_effect = [
|
req.side_effect = [
|
||||||
mresp.mocked_session_send(fake_req),
|
bad_req,
|
||||||
{'authtoken': {'authtoken': 'foobar123'},
|
bad_req,
|
||||||
'networks': {'1234': {'name': 'foobar', 'onboarded': True}}}
|
new_req
|
||||||
]
|
]
|
||||||
self.blink.get_auth_token()
|
self.blink.login_urls = ['test1', 'test2', 'test3']
|
||||||
self.assertEqual(self.blink.region_id, 'piri')
|
self.blink.login_request()
|
||||||
self.assertEqual(self.blink.region, 'UNKNOWN')
|
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
self.assertEqual(self.blink._token, 'foobar123')
|
self.assertEqual(self.blink._login_url, 'test3')
|
||||||
|
|
||||||
|
req.side_effect = [
|
||||||
|
bad_req,
|
||||||
|
new_req,
|
||||||
|
bad_req
|
||||||
|
]
|
||||||
|
self.blink.login_urls = ['test1', 'test2', 'test3']
|
||||||
|
self.blink.login_request()
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
self.assertEqual(self.blink._login_url, 'test2')
|
||||||
|
|
||||||
def test_merge_cameras(self, mock_sess):
|
def test_merge_cameras(self, mock_sess):
|
||||||
"""Test merge camera functionality."""
|
"""Test merge camera functionality."""
|
||||||
first_dict = {'foo': 'bar', 'test': 123}
|
first_dict = {'foo': 'bar', 'test': 123}
|
||||||
next_dict = {'foobar': 456, 'bar': 'foo'}
|
next_dict = {'foobar': 456, 'bar': 'foo'}
|
||||||
self.blink.sync['foo'] = BlinkSyncModule(self.blink, 'foo', 1)
|
self.blink.sync['foo'] = BlinkSyncModule(self.blink, 'foo', 1, [])
|
||||||
self.blink.sync['bar'] = BlinkSyncModule(self.blink, 'bar', 2)
|
self.blink.sync['bar'] = BlinkSyncModule(self.blink, 'bar', 2, [])
|
||||||
self.blink.sync['foo'].cameras = first_dict
|
self.blink.sync['foo'].cameras = first_dict
|
||||||
self.blink.sync['bar'].cameras = next_dict
|
self.blink.sync['bar'].cameras = next_dict
|
||||||
result = self.blink.merge_cameras()
|
result = self.blink.merge_cameras()
|
||||||
expected = {'foo': 'bar', 'test': 123, 'foobar': 456, 'bar': 'foo'}
|
expected = {'foo': 'bar', 'test': 123, 'foobar': 456, 'bar': 'foo'}
|
||||||
self.assertEqual(expected, result)
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
@mock.patch('blinkpy.blinkpy.api.request_videos')
|
||||||
|
def test_download_video_exit(self, mock_req, mock_sess):
|
||||||
|
"""Test we exit method when provided bad response."""
|
||||||
|
blink = blinkpy.Blink()
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
blinkpy._LOGGER.setLevel(logging.DEBUG)
|
||||||
|
blink.last_refresh = 0
|
||||||
|
mock_req.return_value = {}
|
||||||
|
formatted_date = get_time(blink.last_refresh)
|
||||||
|
expected_log = [
|
||||||
|
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
|
||||||
|
formatted_date),
|
||||||
|
"DEBUG:blinkpy.blinkpy:Processing page 1",
|
||||||
|
"INFO:blinkpy.blinkpy:No videos found on page 1. Exiting."
|
||||||
|
]
|
||||||
|
with self.assertLogs() as dl_log:
|
||||||
|
blink.download_videos('/tmp')
|
||||||
|
self.assertEqual(dl_log.output, expected_log)
|
||||||
|
|
||||||
|
@mock.patch('blinkpy.blinkpy.api.request_videos')
|
||||||
|
def test_parse_downloaded_items(self, mock_req, mock_sess):
|
||||||
|
"""Test ability to parse downloaded items list."""
|
||||||
|
blink = blinkpy.Blink()
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
blinkpy._LOGGER.setLevel(logging.DEBUG)
|
||||||
|
generic_entry = {
|
||||||
|
'created_at': '1970',
|
||||||
|
'device_name': 'foo',
|
||||||
|
'deleted': True,
|
||||||
|
'media': '/bar.mp4'
|
||||||
|
}
|
||||||
|
result = [generic_entry]
|
||||||
|
mock_req.return_value = {'media': result}
|
||||||
|
blink.last_refresh = 0
|
||||||
|
formatted_date = get_time(blink.last_refresh)
|
||||||
|
expected_log = [
|
||||||
|
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
|
||||||
|
formatted_date),
|
||||||
|
"DEBUG:blinkpy.blinkpy:Processing page 1",
|
||||||
|
"DEBUG:blinkpy.blinkpy:foo: /bar.mp4 is marked as deleted."
|
||||||
|
]
|
||||||
|
with self.assertLogs() as dl_log:
|
||||||
|
blink.download_videos('/tmp', stop=2)
|
||||||
|
self.assertEqual(dl_log.output, expected_log)
|
||||||
|
|
||||||
|
@mock.patch('blinkpy.blinkpy.api.request_videos')
|
||||||
|
def test_parse_camera_not_in_list(self, mock_req, mock_sess):
|
||||||
|
"""Test ability to parse downloaded items list."""
|
||||||
|
blink = blinkpy.Blink()
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
blinkpy._LOGGER.setLevel(logging.DEBUG)
|
||||||
|
generic_entry = {
|
||||||
|
'created_at': '1970',
|
||||||
|
'device_name': 'foo',
|
||||||
|
'deleted': True,
|
||||||
|
'media': '/bar.mp4'
|
||||||
|
}
|
||||||
|
result = [generic_entry]
|
||||||
|
mock_req.return_value = {'media': result}
|
||||||
|
blink.last_refresh = 0
|
||||||
|
formatted_date = get_time(blink.last_refresh)
|
||||||
|
expected_log = [
|
||||||
|
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
|
||||||
|
formatted_date),
|
||||||
|
"DEBUG:blinkpy.blinkpy:Processing page 1",
|
||||||
|
"DEBUG:blinkpy.blinkpy:Skipping videos for foo."
|
||||||
|
]
|
||||||
|
with self.assertLogs() as dl_log:
|
||||||
|
blink.download_videos('/tmp', camera='bar', stop=2)
|
||||||
|
self.assertEqual(dl_log.output, expected_log)
|
||||||
|
|||||||
+79
-3
@@ -14,6 +14,7 @@ from blinkpy.sync_module import BlinkSyncModule
|
|||||||
from blinkpy.helpers.util import (
|
from blinkpy.helpers.util import (
|
||||||
http_req, create_session, BlinkAuthenticationException,
|
http_req, create_session, BlinkAuthenticationException,
|
||||||
BlinkException, BlinkURLHandler)
|
BlinkException, BlinkURLHandler)
|
||||||
|
from blinkpy.helpers.constants import __version__
|
||||||
import tests.mock_responses as mresp
|
import tests.mock_responses as mresp
|
||||||
|
|
||||||
USERNAME = 'foobar'
|
USERNAME = 'foobar'
|
||||||
@@ -30,7 +31,10 @@ class TestBlinkSetup(unittest.TestCase):
|
|||||||
self.blink_no_cred = Blink()
|
self.blink_no_cred = Blink()
|
||||||
self.blink = Blink(username=USERNAME,
|
self.blink = Blink(username=USERNAME,
|
||||||
password=PASSWORD)
|
password=PASSWORD)
|
||||||
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', '1234')
|
self.blink.sync['test'] = BlinkSyncModule(self.blink,
|
||||||
|
'test',
|
||||||
|
'1234',
|
||||||
|
[])
|
||||||
self.blink.urls = BlinkURLHandler('test')
|
self.blink.urls = BlinkURLHandler('test')
|
||||||
self.blink.session = create_session()
|
self.blink.session = create_session()
|
||||||
|
|
||||||
@@ -41,6 +45,7 @@ class TestBlinkSetup(unittest.TestCase):
|
|||||||
|
|
||||||
def test_initialization(self, mock_sess):
|
def test_initialization(self, mock_sess):
|
||||||
"""Verify we can initialize blink."""
|
"""Verify we can initialize blink."""
|
||||||
|
self.assertEqual(self.blink.version, __version__)
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
self.assertEqual(self.blink._username, USERNAME)
|
self.assertEqual(self.blink._username, USERNAME)
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
@@ -74,6 +79,47 @@ class TestBlinkSetup(unittest.TestCase):
|
|||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
self.assertEqual(self.blink_no_cred._password, PASSWORD)
|
self.assertEqual(self.blink_no_cred._password, PASSWORD)
|
||||||
|
|
||||||
|
@mock.patch('blinkpy.blinkpy.getpass.getpass')
|
||||||
|
@mock.patch('blinkpy.blinkpy.Blink.get_auth_token')
|
||||||
|
def test_no_cred_file(self, getpwd, getauth, mock_sess):
|
||||||
|
"""Check that normal login occurs when cred file doesn't exist."""
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
self.blink._cred_file = '/tmp/fake.file'
|
||||||
|
getpwd.return_value = PASSWORD
|
||||||
|
getauth.return_value = True
|
||||||
|
with mock.patch('builtins.input', return_value=USERNAME):
|
||||||
|
self.assertTrue(self.blink.login())
|
||||||
|
|
||||||
|
def test_exit_on_missing_json(self, mock_sess):
|
||||||
|
"""Test that we fail on missing json data."""
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
self.blink._cred_file = '/tmp/fake.file'
|
||||||
|
with mock.patch('os.path.isfile', return_value=True):
|
||||||
|
with mock.patch('builtins.open', mock.mock_open(read_data="{}")):
|
||||||
|
self.assertFalse(self.blink.login())
|
||||||
|
|
||||||
|
def test_exit_on_bad_json(self, mock_sess):
|
||||||
|
"""Test that we fail on bad json format."""
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
self.blink._cred_file = '/tmp/fake.file'
|
||||||
|
with mock.patch('os.path.isfile', return_value=True):
|
||||||
|
with mock.patch('builtins.open', mock.mock_open(read_data='{]')):
|
||||||
|
self.assertFalse(self.blink.login())
|
||||||
|
|
||||||
|
@mock.patch('blinkpy.blinkpy.json.load')
|
||||||
|
def test_cred_file(self, mockjson, mock_sess):
|
||||||
|
"""Test that loading credential file works."""
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
self.blink_no_cred._cred_file = '/tmp/fake.file'
|
||||||
|
mockjson.return_value = {'username': 'foo', 'password': 'bar'}
|
||||||
|
with mock.patch('os.path.isfile', return_value=True):
|
||||||
|
with mock.patch('builtins.open', mock.mock_open(read_data='')):
|
||||||
|
self.assertTrue(self.blink_no_cred.login())
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
self.assertEqual(self.blink_no_cred._username, 'foo')
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
self.assertEqual(self.blink_no_cred._password, 'bar')
|
||||||
|
|
||||||
def test_bad_request(self, mock_sess):
|
def test_bad_request(self, mock_sess):
|
||||||
"""Check that we raise an Exception with a bad request."""
|
"""Check that we raise an Exception with a bad request."""
|
||||||
self.blink.session = create_session()
|
self.blink.session = create_session()
|
||||||
@@ -138,9 +184,13 @@ class TestBlinkSetup(unittest.TestCase):
|
|||||||
now = self.blink.refresh_rate + 1
|
now = self.blink.refresh_rate + 1
|
||||||
mock_time.return_value = now
|
mock_time.return_value = now
|
||||||
self.assertEqual(self.blink.last_refresh, None)
|
self.assertEqual(self.blink.last_refresh, None)
|
||||||
result = self.blink.check_if_ok_to_update()
|
self.assertEqual(self.blink.check_if_ok_to_update(), True)
|
||||||
|
self.assertEqual(self.blink.last_refresh, None)
|
||||||
|
with mock.patch('blinkpy.sync_module.BlinkSyncModule.refresh',
|
||||||
|
return_value=True):
|
||||||
|
self.blink.refresh()
|
||||||
|
|
||||||
self.assertEqual(self.blink.last_refresh, now)
|
self.assertEqual(self.blink.last_refresh, now)
|
||||||
self.assertEqual(result, True)
|
|
||||||
self.assertEqual(self.blink.check_if_ok_to_update(), False)
|
self.assertEqual(self.blink.check_if_ok_to_update(), False)
|
||||||
self.assertEqual(self.blink.last_refresh, now)
|
self.assertEqual(self.blink.last_refresh, now)
|
||||||
|
|
||||||
@@ -148,3 +198,29 @@ class TestBlinkSetup(unittest.TestCase):
|
|||||||
"""Check that we can access sync modules ignoring case."""
|
"""Check that we can access sync modules ignoring case."""
|
||||||
self.assertEqual(self.blink.sync['test'].name, 'test')
|
self.assertEqual(self.blink.sync['test'].name, 'test')
|
||||||
self.assertEqual(self.blink.sync['TEST'].name, 'test')
|
self.assertEqual(self.blink.sync['TEST'].name, 'test')
|
||||||
|
|
||||||
|
@mock.patch('blinkpy.api.request_login')
|
||||||
|
def test_unexpected_login(self, mock_login, mock_sess):
|
||||||
|
"""Check that we appropriately handle unexpected login info."""
|
||||||
|
mock_login.return_value = None
|
||||||
|
self.assertFalse(self.blink.get_auth_token())
|
||||||
|
|
||||||
|
@mock.patch('blinkpy.api.request_homescreen')
|
||||||
|
def test_get_cameras(self, mock_home, mock_sess):
|
||||||
|
"""Check retrieval of camera information."""
|
||||||
|
mock_home.return_value = {
|
||||||
|
'cameras': [{'name': 'foo', 'network_id': 1234, 'id': 5678},
|
||||||
|
{'name': 'bar', 'network_id': 1234, 'id': 5679},
|
||||||
|
{'name': 'test', 'network_id': 4321, 'id': 0000}]
|
||||||
|
}
|
||||||
|
result = self.blink.get_cameras()
|
||||||
|
self.assertEqual(result, {'1234': [{'name': 'foo', 'id': 5678},
|
||||||
|
{'name': 'bar', 'id': 5679}],
|
||||||
|
'4321': [{'name': 'test', 'id': 0000}]})
|
||||||
|
|
||||||
|
@mock.patch('blinkpy.api.request_homescreen')
|
||||||
|
def test_get_cameras_failure(self, mock_home, mock_sess):
|
||||||
|
"""Check that on failure we initialize empty info and move on."""
|
||||||
|
mock_home.return_value = {}
|
||||||
|
result = self.blink.get_cameras()
|
||||||
|
self.assertEqual(result, {})
|
||||||
|
|||||||
+22
-75
@@ -11,7 +11,7 @@ from unittest import mock
|
|||||||
from blinkpy import blinkpy
|
from blinkpy import blinkpy
|
||||||
from blinkpy.helpers.util import create_session, BlinkURLHandler
|
from blinkpy.helpers.util import create_session, BlinkURLHandler
|
||||||
from blinkpy.sync_module import BlinkSyncModule
|
from blinkpy.sync_module import BlinkSyncModule
|
||||||
from blinkpy.camera import BlinkCamera, MAX_CLIPS
|
from blinkpy.camera import BlinkCamera
|
||||||
import tests.mock_responses as mresp
|
import tests.mock_responses as mresp
|
||||||
|
|
||||||
USERNAME = 'foobar'
|
USERNAME = 'foobar'
|
||||||
@@ -46,7 +46,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
|||||||
self.blink._auth_header = header
|
self.blink._auth_header = header
|
||||||
self.blink.session = create_session()
|
self.blink.session = create_session()
|
||||||
self.blink.urls = BlinkURLHandler('test')
|
self.blink.urls = BlinkURLHandler('test')
|
||||||
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', 1234)
|
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', 1234, [])
|
||||||
self.camera = BlinkCamera(self.blink.sync['test'])
|
self.camera = BlinkCamera(self.blink.sync['test'])
|
||||||
self.camera.name = 'foobar'
|
self.camera.name = 'foobar'
|
||||||
self.blink.sync['test'].cameras['foobar'] = self.camera
|
self.blink.sync['test'].cameras['foobar'] = self.camera
|
||||||
@@ -55,37 +55,11 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
|||||||
"""Clean up after test."""
|
"""Clean up after test."""
|
||||||
self.blink = None
|
self.blink = None
|
||||||
|
|
||||||
def test_check_for_motion(self, mock_sess):
|
|
||||||
"""Test check for motion function."""
|
|
||||||
self.assertEqual(self.camera.last_record, [])
|
|
||||||
self.assertEqual(self.camera.motion_detected, None)
|
|
||||||
self.camera.sync.record_dates = {'foobar': [1, 3, 2, 4]}
|
|
||||||
self.camera.check_for_motion()
|
|
||||||
self.assertEqual(self.camera.last_record, [4])
|
|
||||||
self.assertEqual(self.camera.motion_detected, False)
|
|
||||||
self.camera.sync.record_dates = {'foobar': [7, 1, 3, 4]}
|
|
||||||
self.camera.check_for_motion()
|
|
||||||
self.assertEqual(self.camera.last_record, [7, 4])
|
|
||||||
self.assertEqual(self.camera.motion_detected, True)
|
|
||||||
self.camera.check_for_motion()
|
|
||||||
self.assertEqual(self.camera.last_record, [7, 4])
|
|
||||||
self.assertEqual(self.camera.motion_detected, False)
|
|
||||||
|
|
||||||
def test_max_motion_clips(self, mock_sess):
|
|
||||||
"""Test that we only maintain certain number of records."""
|
|
||||||
for i in range(0, MAX_CLIPS):
|
|
||||||
self.camera.last_record.append(i)
|
|
||||||
self.camera.sync.record_dates['foobar'] = [MAX_CLIPS+2]
|
|
||||||
self.assertEqual(len(self.camera.last_record), MAX_CLIPS)
|
|
||||||
self.camera.check_for_motion()
|
|
||||||
self.assertEqual(self.camera.motion_detected, True)
|
|
||||||
self.assertEqual(len(self.camera.last_record), MAX_CLIPS)
|
|
||||||
|
|
||||||
def test_camera_update(self, mock_sess):
|
def test_camera_update(self, mock_sess):
|
||||||
"""Test that we can properly update camera properties."""
|
"""Test that we can properly update camera properties."""
|
||||||
config = {
|
config = {
|
||||||
'name': 'new',
|
'name': 'new',
|
||||||
'camera_id': 1234,
|
'id': 1234,
|
||||||
'network_id': 5678,
|
'network_id': 5678,
|
||||||
'serial': '12345678',
|
'serial': '12345678',
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
@@ -96,7 +70,12 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
|||||||
'thumbnail': '/thumb',
|
'thumbnail': '/thumb',
|
||||||
}
|
}
|
||||||
self.camera.last_record = ['1']
|
self.camera.last_record = ['1']
|
||||||
self.camera.sync.all_clips = {'new': {'1': '/test.mp4'}}
|
self.camera.sync.last_record = {
|
||||||
|
'new': {
|
||||||
|
'clip': '/test.mp4',
|
||||||
|
'time': '1970-01-01T00:00:00'
|
||||||
|
}
|
||||||
|
}
|
||||||
mock_sess.side_effect = [
|
mock_sess.side_effect = [
|
||||||
mresp.MockResponse({'temp': 71}, 200),
|
mresp.MockResponse({'temp': 71}, 200),
|
||||||
'test',
|
'test',
|
||||||
@@ -108,62 +87,25 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
|||||||
self.assertEqual(self.camera.network_id, '5678')
|
self.assertEqual(self.camera.network_id, '5678')
|
||||||
self.assertEqual(self.camera.serial, '12345678')
|
self.assertEqual(self.camera.serial, '12345678')
|
||||||
self.assertEqual(self.camera.motion_enabled, False)
|
self.assertEqual(self.camera.motion_enabled, False)
|
||||||
self.assertEqual(self.camera.battery, 50)
|
self.assertEqual(self.camera.battery, 'ok')
|
||||||
self.assertEqual(self.camera.temperature, 68)
|
self.assertEqual(self.camera.temperature, 68)
|
||||||
self.assertEqual(self.camera.temperature_c, 20)
|
self.assertEqual(self.camera.temperature_c, 20)
|
||||||
self.assertEqual(self.camera.temperature_calibrated, 71)
|
self.assertEqual(self.camera.temperature_calibrated, 71)
|
||||||
self.assertEqual(self.camera.wifi_strength, 4)
|
self.assertEqual(self.camera.wifi_strength, 4)
|
||||||
self.assertEqual(self.camera.thumbnail,
|
self.assertEqual(self.camera.thumbnail,
|
||||||
'https://rest.test.immedia-semi.com/thumb.jpg')
|
'https://rest-test.immedia-semi.com/thumb.jpg')
|
||||||
self.assertEqual(self.camera.clip,
|
self.assertEqual(self.camera.clip,
|
||||||
'https://rest.test.immedia-semi.com/test.mp4')
|
'https://rest-test.immedia-semi.com/test.mp4')
|
||||||
self.assertEqual(self.camera.image_from_cache, 'test')
|
self.assertEqual(self.camera.image_from_cache, 'test')
|
||||||
self.assertEqual(self.camera.video_from_cache, 'foobar')
|
self.assertEqual(self.camera.video_from_cache, 'foobar')
|
||||||
|
|
||||||
def test_thumbnail_not_in_info(self, mock_sess):
|
|
||||||
"""Test that we grab thumbanil if not in camera_info."""
|
|
||||||
mock_sess.side_effect = [
|
|
||||||
mresp.MockResponse({'temp': 71}, 200),
|
|
||||||
'foobar',
|
|
||||||
'barfoo'
|
|
||||||
]
|
|
||||||
self.camera.last_record = ['1']
|
|
||||||
self.camera.sync.record_dates['new'] = ['1']
|
|
||||||
self.camera.sync.all_clips = {'new': {'1': '/test.mp4'}}
|
|
||||||
config = {
|
|
||||||
'name': 'new',
|
|
||||||
'camera_id': 1234,
|
|
||||||
'network_id': 5678,
|
|
||||||
'serial': '12345678',
|
|
||||||
'enabled': False,
|
|
||||||
'battery_voltage': 90,
|
|
||||||
'battery_state': 'ok',
|
|
||||||
'temperature': 68,
|
|
||||||
'wifi_strength': 4,
|
|
||||||
'thumbnail': '',
|
|
||||||
}
|
|
||||||
self.camera.sync.homescreen = {
|
|
||||||
'devices': [
|
|
||||||
{'foo': 'bar'},
|
|
||||||
{'device_type': 'foobar'},
|
|
||||||
{'device_type': 'camera',
|
|
||||||
'name': 'new',
|
|
||||||
'thumbnail': '/new/thumb'}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
self.camera.update(config)
|
|
||||||
self.assertEqual(self.camera.thumbnail,
|
|
||||||
'https://rest.test.immedia-semi.com/new/thumb.jpg')
|
|
||||||
|
|
||||||
def test_no_thumbnails(self, mock_sess):
|
def test_no_thumbnails(self, mock_sess):
|
||||||
"""Tests that thumbnail is 'None' if none found."""
|
"""Tests that thumbnail is 'None' if none found."""
|
||||||
mock_sess.return_value = 'foobar'
|
mock_sess.return_value = 'foobar'
|
||||||
self.camera.last_record = ['1']
|
self.camera.last_record = ['1']
|
||||||
self.camera.sync.record_dates['new'] = ['1']
|
|
||||||
self.camera.sync.all_clips = {'new': {'1': '/test.mp4'}}
|
|
||||||
config = {
|
config = {
|
||||||
'name': 'new',
|
'name': 'new',
|
||||||
'camera_id': 1234,
|
'id': 1234,
|
||||||
'network_id': 5678,
|
'network_id': 5678,
|
||||||
'serial': '12345678',
|
'serial': '12345678',
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
@@ -176,13 +118,18 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
|||||||
self.camera.sync.homescreen = {
|
self.camera.sync.homescreen = {
|
||||||
'devices': []
|
'devices': []
|
||||||
}
|
}
|
||||||
|
self.assertEqual(self.camera.temperature_calibrated, None)
|
||||||
with self.assertLogs() as logrecord:
|
with self.assertLogs() as logrecord:
|
||||||
self.camera.update(config)
|
self.camera.update(config, force=True)
|
||||||
self.assertEqual(self.camera.thumbnail, None)
|
self.assertEqual(self.camera.thumbnail, None)
|
||||||
|
self.assertEqual(self.camera.last_record, ['1'])
|
||||||
|
self.assertEqual(self.camera.temperature_calibrated, 68)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
logrecord.output,
|
logrecord.output,
|
||||||
["ERROR:blinkpy.camera:Could not retrieve calibrated temperature.",
|
[("WARNING:blinkpy.camera:Could not retrieve calibrated "
|
||||||
"ERROR:blinkpy.camera:Could not find thumbnail for camera new"]
|
"temperature."),
|
||||||
|
("WARNING:blinkpy.camera:Could not find thumbnail for camera new"
|
||||||
|
"\nNoneType: None")]
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_no_video_clips(self, mock_sess):
|
def test_no_video_clips(self, mock_sess):
|
||||||
@@ -190,7 +137,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
|||||||
mock_sess.return_value = 'foobar'
|
mock_sess.return_value = 'foobar'
|
||||||
config = {
|
config = {
|
||||||
'name': 'new',
|
'name': 'new',
|
||||||
'camera_id': 1234,
|
'id': 1234,
|
||||||
'network_id': 5678,
|
'network_id': 5678,
|
||||||
'serial': '12345678',
|
'serial': '12345678',
|
||||||
'enabled': False,
|
'enabled': False,
|
||||||
|
|||||||
+183
-58
@@ -17,69 +17,21 @@ class TestBlinkSyncModule(unittest.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Set up Blink module."""
|
"""Set up Blink module."""
|
||||||
self.blink = blinkpy.Blink(username=USERNAME,
|
self.blink = blinkpy.Blink(username=USERNAME,
|
||||||
password=PASSWORD)
|
password=PASSWORD,
|
||||||
|
motion_interval=0)
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
self.blink._auth_header = {
|
self.blink._auth_header = {
|
||||||
'Host': 'test.url.tld',
|
'Host': 'test.url.tld',
|
||||||
'TOKEN_AUTH': 'foobar123'
|
'TOKEN_AUTH': 'foobar123'
|
||||||
}
|
}
|
||||||
|
self.blink.last_refresh = 0
|
||||||
self.blink.urls = blinkpy.BlinkURLHandler('test')
|
self.blink.urls = blinkpy.BlinkURLHandler('test')
|
||||||
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', '1234')
|
self.blink.sync['test'] = BlinkSyncModule(self.blink,
|
||||||
|
'test',
|
||||||
|
'1234',
|
||||||
|
[])
|
||||||
self.camera = BlinkCamera(self.blink.sync)
|
self.camera = BlinkCamera(self.blink.sync)
|
||||||
|
self.mock_start = [
|
||||||
def tearDown(self):
|
|
||||||
"""Clean up after test."""
|
|
||||||
self.blink = None
|
|
||||||
self.camera = None
|
|
||||||
|
|
||||||
def test_get_events(self, mock_resp):
|
|
||||||
"""Test get events function."""
|
|
||||||
mock_resp.return_value = {'event': True}
|
|
||||||
self.assertEqual(self.blink.sync['test'].get_events(), True)
|
|
||||||
|
|
||||||
def test_get_camera_info(self, mock_resp):
|
|
||||||
"""Test get camera info function."""
|
|
||||||
mock_resp.return_value = {'devicestatus': True}
|
|
||||||
self.assertEqual(self.blink.sync['test'].get_camera_info(), True)
|
|
||||||
|
|
||||||
def test_get_videos_one_page(self, mock_resp):
|
|
||||||
"""Test video access."""
|
|
||||||
mock_resp.return_value = [
|
|
||||||
{
|
|
||||||
'camera_name': 'foobar',
|
|
||||||
'address': '/test/clip_1900_01_01_12_00_00AM.mp4',
|
|
||||||
'thumbnail': '/test/thumb'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
expected_videos = {'foobar': [
|
|
||||||
{'clip': '/test/clip_1900_01_01_12_00_00AM.mp4',
|
|
||||||
'thumb': '/test/thumb'}]}
|
|
||||||
expected_recs = {'foobar': ['1900_01_01_12_00_00AM']}
|
|
||||||
expected_clips = {'foobar': {
|
|
||||||
'1900_01_01_12_00_00AM': '/test/clip_1900_01_01_12_00_00AM.mp4'}}
|
|
||||||
self.blink.sync['test'].get_videos(start_page=0, end_page=0)
|
|
||||||
self.assertEqual(self.blink.sync['test'].videos, expected_videos)
|
|
||||||
self.assertEqual(self.blink.sync['test'].record_dates, expected_recs)
|
|
||||||
self.assertEqual(self.blink.sync['test'].all_clips, expected_clips)
|
|
||||||
|
|
||||||
def test_get_videos_multi_page(self, mock_resp):
|
|
||||||
"""Test video access with multiple pages."""
|
|
||||||
mock_resp.return_value = [
|
|
||||||
{
|
|
||||||
'camera_name': 'test',
|
|
||||||
'address': '/foo/bar_1900_01_01_12_00_00AM.mp4',
|
|
||||||
'thumbnail': '/foobar'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
self.blink.sync['test'].get_videos()
|
|
||||||
self.assertEqual(mock_resp.call_count, 2)
|
|
||||||
mock_resp.reset_mock()
|
|
||||||
self.blink.sync['test'].get_videos(start_page=0, end_page=9)
|
|
||||||
self.assertEqual(mock_resp.call_count, 10)
|
|
||||||
|
|
||||||
def test_sync_start(self, mock_resp):
|
|
||||||
"""Test sync start function."""
|
|
||||||
mock_resp.side_effect = [
|
|
||||||
{'syncmodule': {
|
{'syncmodule': {
|
||||||
'id': 1234,
|
'id': 1234,
|
||||||
'network_id': 5678,
|
'network_id': 5678,
|
||||||
@@ -88,13 +40,186 @@ class TestBlinkSyncModule(unittest.TestCase):
|
|||||||
{'event': True},
|
{'event': True},
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
{'devicestatus': {}},
|
|
||||||
None,
|
None,
|
||||||
None
|
{'devicestatus': {}},
|
||||||
]
|
]
|
||||||
|
self.blink.sync['test'].network_info = {'network': {'armed': True}}
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Clean up after test."""
|
||||||
|
self.blink = None
|
||||||
|
self.camera = None
|
||||||
|
self.mock_start = None
|
||||||
|
|
||||||
|
def test_get_events(self, mock_resp):
|
||||||
|
"""Test get events function."""
|
||||||
|
mock_resp.return_value = {'event': True}
|
||||||
|
self.assertEqual(self.blink.sync['test'].get_events(), True)
|
||||||
|
|
||||||
|
def test_get_camera_info(self, mock_resp):
|
||||||
|
"""Test get camera info function."""
|
||||||
|
mock_resp.return_value = {'camera': ['foobar']}
|
||||||
|
self.assertEqual(self.blink.sync['test'].get_camera_info('1234'),
|
||||||
|
'foobar')
|
||||||
|
|
||||||
|
def test_check_new_videos_startup(self, mock_resp):
|
||||||
|
"""Test that check_new_videos does not block startup."""
|
||||||
|
sync_module = self.blink.sync['test']
|
||||||
|
self.blink.last_refresh = None
|
||||||
|
self.assertFalse(sync_module.check_new_videos())
|
||||||
|
|
||||||
|
def test_check_new_videos(self, mock_resp):
|
||||||
|
"""Test recent video response."""
|
||||||
|
mock_resp.return_value = {
|
||||||
|
'media': [{
|
||||||
|
'device_name': 'foo',
|
||||||
|
'media': '/foo/bar.mp4',
|
||||||
|
'created_at': '1990-01-01T00:00:00+00:00'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_module = self.blink.sync['test']
|
||||||
|
sync_module.cameras = {'foo': None}
|
||||||
|
sync_module.blink.last_refresh = 0
|
||||||
|
self.assertEqual(sync_module.motion, {})
|
||||||
|
self.assertTrue(sync_module.check_new_videos())
|
||||||
|
self.assertEqual(sync_module.last_record['foo'],
|
||||||
|
{'clip': '/foo/bar.mp4',
|
||||||
|
'time': '1990-01-01T00:00:00+00:00'})
|
||||||
|
self.assertEqual(sync_module.motion, {'foo': True})
|
||||||
|
mock_resp.return_value = {'media': []}
|
||||||
|
self.assertTrue(sync_module.check_new_videos())
|
||||||
|
self.assertEqual(sync_module.motion, {'foo': False})
|
||||||
|
self.assertEqual(sync_module.last_record['foo'],
|
||||||
|
{'clip': '/foo/bar.mp4',
|
||||||
|
'time': '1990-01-01T00:00:00+00:00'})
|
||||||
|
|
||||||
|
def test_check_new_videos_old_date(self, mock_resp):
|
||||||
|
"""Test videos return response with old date."""
|
||||||
|
mock_resp.return_value = {
|
||||||
|
'media': [{
|
||||||
|
'device_name': 'foo',
|
||||||
|
'media': '/foo/bar.mp4',
|
||||||
|
'created_at': '1970-01-01T00:00:00+00:00'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_module = self.blink.sync['test']
|
||||||
|
sync_module.cameras = {'foo': None}
|
||||||
|
sync_module.blink.last_refresh = 1000
|
||||||
|
self.assertTrue(sync_module.check_new_videos())
|
||||||
|
self.assertEqual(sync_module.motion, {'foo': False})
|
||||||
|
|
||||||
|
def test_check_no_motion_if_not_armed(self, mock_resp):
|
||||||
|
"""Test that motion detection is not set if module unarmed."""
|
||||||
|
mock_resp.return_value = {
|
||||||
|
'media': [{
|
||||||
|
'device_name': 'foo',
|
||||||
|
'media': '/foo/bar.mp4',
|
||||||
|
'created_at': '1990-01-01T00:00:00+00:00'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
sync_module = self.blink.sync['test']
|
||||||
|
sync_module.cameras = {'foo': None}
|
||||||
|
sync_module.blink.last_refresh = 1000
|
||||||
|
self.assertTrue(sync_module.check_new_videos())
|
||||||
|
self.assertEqual(sync_module.motion, {'foo': True})
|
||||||
|
sync_module.network_info = {'network': {'armed': False}}
|
||||||
|
self.assertTrue(sync_module.check_new_videos())
|
||||||
|
self.assertEqual(sync_module.motion, {'foo': False})
|
||||||
|
|
||||||
|
def test_check_multiple_videos(self, mock_resp):
|
||||||
|
"""Test motion found even with multiple videos."""
|
||||||
|
mock_resp.return_value = {
|
||||||
|
'media': [
|
||||||
|
{
|
||||||
|
'device_name': 'foo',
|
||||||
|
'media': '/foo/bar.mp4',
|
||||||
|
'created_at': '1970-01-01T00:00:00+00:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'device_name': 'foo',
|
||||||
|
'media': '/bar/foo.mp4',
|
||||||
|
'created_at': '1990-01-01T00:00:00+00:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'device_name': 'foo',
|
||||||
|
'media': '/foobar.mp4',
|
||||||
|
'created_at': '1970-01-01T00:00:01+00:00'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
sync_module = self.blink.sync['test']
|
||||||
|
sync_module.cameras = {'foo': None}
|
||||||
|
sync_module.blink.last_refresh = 1000
|
||||||
|
self.assertTrue(sync_module.check_new_videos())
|
||||||
|
self.assertEqual(sync_module.motion, {'foo': True})
|
||||||
|
expected_result = {
|
||||||
|
'foo': {
|
||||||
|
'clip': '/bar/foo.mp4',
|
||||||
|
'time': '1990-01-01T00:00:00+00:00'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.assertEqual(sync_module.last_record, expected_result)
|
||||||
|
|
||||||
|
def test_check_new_videos_failed(self, mock_resp):
|
||||||
|
"""Test method when response is unexpected."""
|
||||||
|
mock_resp.side_effect = [None, 'just a string', {}]
|
||||||
|
sync_module = self.blink.sync['test']
|
||||||
|
sync_module.cameras = {'foo': None}
|
||||||
|
|
||||||
|
sync_module.motion['foo'] = True
|
||||||
|
self.assertFalse(sync_module.check_new_videos())
|
||||||
|
self.assertFalse(sync_module.motion['foo'])
|
||||||
|
|
||||||
|
sync_module.motion['foo'] = True
|
||||||
|
self.assertFalse(sync_module.check_new_videos())
|
||||||
|
self.assertFalse(sync_module.motion['foo'])
|
||||||
|
|
||||||
|
sync_module.motion['foo'] = True
|
||||||
|
self.assertFalse(sync_module.check_new_videos())
|
||||||
|
self.assertFalse(sync_module.motion['foo'])
|
||||||
|
|
||||||
|
def test_sync_start(self, mock_resp):
|
||||||
|
"""Test sync start function."""
|
||||||
|
mock_resp.side_effect = self.mock_start
|
||||||
self.blink.sync['test'].start()
|
self.blink.sync['test'].start()
|
||||||
self.assertEqual(self.blink.sync['test'].name, 'test')
|
self.assertEqual(self.blink.sync['test'].name, 'test')
|
||||||
self.assertEqual(self.blink.sync['test'].sync_id, 1234)
|
self.assertEqual(self.blink.sync['test'].sync_id, 1234)
|
||||||
self.assertEqual(self.blink.sync['test'].network_id, 5678)
|
self.assertEqual(self.blink.sync['test'].network_id, 5678)
|
||||||
self.assertEqual(self.blink.sync['test'].serial, '12345678')
|
self.assertEqual(self.blink.sync['test'].serial, '12345678')
|
||||||
self.assertEqual(self.blink.sync['test'].status, 'foobar')
|
self.assertEqual(self.blink.sync['test'].status, 'foobar')
|
||||||
|
|
||||||
|
def test_unexpected_summary(self, mock_resp):
|
||||||
|
"""Test unexpected summary response."""
|
||||||
|
self.mock_start[0] = None
|
||||||
|
mock_resp.side_effect = self.mock_start
|
||||||
|
self.assertFalse(self.blink.sync['test'].start())
|
||||||
|
|
||||||
|
def test_summary_with_no_network_id(self, mock_resp):
|
||||||
|
"""Test handling of bad summary."""
|
||||||
|
self.mock_start[0]['syncmodule'] = None
|
||||||
|
mock_resp.side_effect = self.mock_start
|
||||||
|
self.assertFalse(self.blink.sync['test'].start())
|
||||||
|
|
||||||
|
def test_summary_with_only_network_id(self, mock_resp):
|
||||||
|
"""Test handling of sparse summary."""
|
||||||
|
self.mock_start[0]['syncmodule'] = {'network_id': 8675309}
|
||||||
|
mock_resp.side_effect = self.mock_start
|
||||||
|
self.blink.sync['test'].start()
|
||||||
|
self.assertEqual(self.blink.sync['test'].network_id, 8675309)
|
||||||
|
|
||||||
|
def test_unexpected_camera_info(self, mock_resp):
|
||||||
|
"""Test unexpected camera info response."""
|
||||||
|
self.blink.sync['test'].cameras['foo'] = None
|
||||||
|
self.mock_start[5] = None
|
||||||
|
mock_resp.side_effect = self.mock_start
|
||||||
|
self.blink.sync['test'].start()
|
||||||
|
self.assertEqual(self.blink.sync['test'].cameras, {'foo': None})
|
||||||
|
|
||||||
|
def test_missing_camera_info(self, mock_resp):
|
||||||
|
"""Test missing key from camera info response."""
|
||||||
|
self.blink.sync['test'].cameras['foo'] = None
|
||||||
|
self.mock_start[5] = {}
|
||||||
|
self.blink.sync['test'].start()
|
||||||
|
self.assertEqual(self.blink.sync['test'].cameras, {'foo': None})
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"""Test various api functions."""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
import time
|
||||||
|
from blinkpy.helpers.util import Throttle, BlinkURLHandler, time_to_seconds
|
||||||
|
|
||||||
|
|
||||||
|
class TestUtil(unittest.TestCase):
|
||||||
|
"""Test the helpers/util module."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Initialize the blink module."""
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Tear down blink module."""
|
||||||
|
|
||||||
|
def test_throttle(self):
|
||||||
|
"""Test the throttle decorator."""
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
@Throttle(seconds=5)
|
||||||
|
def test_throttle():
|
||||||
|
calls.append(1)
|
||||||
|
|
||||||
|
now = int(time.time())
|
||||||
|
now_plus_four = now + 4
|
||||||
|
now_plus_six = now + 6
|
||||||
|
|
||||||
|
test_throttle()
|
||||||
|
self.assertEqual(1, len(calls))
|
||||||
|
|
||||||
|
# Call again, still shouldn't fire
|
||||||
|
test_throttle()
|
||||||
|
self.assertEqual(1, len(calls))
|
||||||
|
|
||||||
|
# Call with force
|
||||||
|
test_throttle(force=True)
|
||||||
|
self.assertEqual(2, len(calls))
|
||||||
|
|
||||||
|
# Call without throttle, shouldn't fire
|
||||||
|
test_throttle()
|
||||||
|
self.assertEqual(2, len(calls))
|
||||||
|
|
||||||
|
# Fake time as 4 seconds from now
|
||||||
|
with mock.patch('time.time', return_value=now_plus_four):
|
||||||
|
test_throttle()
|
||||||
|
self.assertEqual(2, len(calls))
|
||||||
|
|
||||||
|
# Fake time as 6 seconds from now
|
||||||
|
with mock.patch('time.time', return_value=now_plus_six):
|
||||||
|
test_throttle()
|
||||||
|
self.assertEqual(3, len(calls))
|
||||||
|
|
||||||
|
def test_throttle_per_instance(self):
|
||||||
|
"""Test that throttle is done once per instance of class."""
|
||||||
|
class Tester:
|
||||||
|
"""A tester class for throttling."""
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
"""Test the throttle."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
tester = Tester()
|
||||||
|
throttled = Throttle(seconds=1)(tester.test)
|
||||||
|
self.assertEqual(throttled(), True)
|
||||||
|
self.assertEqual(throttled(), None)
|
||||||
|
|
||||||
|
def test_throttle_on_two_methods(self):
|
||||||
|
"""Test that throttle works for multiple methods."""
|
||||||
|
class Tester:
|
||||||
|
"""A tester class for throttling."""
|
||||||
|
|
||||||
|
@Throttle(seconds=3)
|
||||||
|
def test1(self):
|
||||||
|
"""Test function for throttle."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@Throttle(seconds=5)
|
||||||
|
def test2(self):
|
||||||
|
"""Test function for throttle."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
tester = Tester()
|
||||||
|
now = time.time()
|
||||||
|
now_plus_4 = now + 4
|
||||||
|
now_plus_6 = now + 6
|
||||||
|
|
||||||
|
self.assertEqual(tester.test1(), True)
|
||||||
|
self.assertEqual(tester.test2(), True)
|
||||||
|
self.assertEqual(tester.test1(), None)
|
||||||
|
self.assertEqual(tester.test2(), None)
|
||||||
|
|
||||||
|
with mock.patch('time.time', return_value=now_plus_4):
|
||||||
|
self.assertEqual(tester.test1(), True)
|
||||||
|
self.assertEqual(tester.test2(), None)
|
||||||
|
|
||||||
|
with mock.patch('time.time', return_value=now_plus_6):
|
||||||
|
self.assertEqual(tester.test1(), None)
|
||||||
|
self.assertEqual(tester.test2(), True)
|
||||||
|
|
||||||
|
def test_legacy_subdomains(self):
|
||||||
|
"""Test that subdomain can be set to legacy mode."""
|
||||||
|
urls = BlinkURLHandler('test')
|
||||||
|
self.assertEqual(urls.subdomain, 'rest-test')
|
||||||
|
urls = BlinkURLHandler('test', legacy=True)
|
||||||
|
self.assertEqual(urls.subdomain, 'rest.test')
|
||||||
|
|
||||||
|
def test_time_to_seconds(self):
|
||||||
|
"""Test time to seconds conversion."""
|
||||||
|
correct_time = '1970-01-01T00:00:05+00:00'
|
||||||
|
wrong_time = '1/1/1970 00:00:03'
|
||||||
|
self.assertEqual(time_to_seconds(correct_time), 5)
|
||||||
|
self.assertFalse(time_to_seconds(wrong_time))
|
||||||
@@ -8,11 +8,21 @@ setenv =
|
|||||||
LANG=en_US.UTF-8
|
LANG=en_US.UTF-8
|
||||||
PYTHONPATH = {toxinidir}
|
PYTHONPATH = {toxinidir}
|
||||||
commands =
|
commands =
|
||||||
py.test --timeout=30 --duration=10 --cov=blinkpy --cov-report term-missing {posargs}
|
pytest --timeout=9 --durations=10
|
||||||
deps =
|
deps =
|
||||||
-r{toxinidir}/requirements.txt
|
-r{toxinidir}/requirements.txt
|
||||||
-r{toxinidir}/requirements_test.txt
|
-r{toxinidir}/requirements_test.txt
|
||||||
|
|
||||||
|
[testenv:cov]
|
||||||
|
setenv =
|
||||||
|
LANG=en_US.UTF-8
|
||||||
|
PYTHONPATH = {toxinidir}
|
||||||
|
commands =
|
||||||
|
pytest --timeout=9 --durations=10 --cov=blinkpy --cov-report=xml {posargs}
|
||||||
|
deps =
|
||||||
|
-r{toxinidir}/requirements.txt
|
||||||
|
-r{toxinidir}/requirements_test.txt
|
||||||
|
|
||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
deps =
|
deps =
|
||||||
-r{toxinidir}/requirements.txt
|
-r{toxinidir}/requirements.txt
|
||||||
@@ -20,9 +30,9 @@ deps =
|
|||||||
basepython = python3
|
basepython = python3
|
||||||
ignore_errors = True
|
ignore_errors = True
|
||||||
commands =
|
commands =
|
||||||
pylint --rcfile={toxinidir}/pylintrc blinkpy tests
|
pylint --rcfile={toxinidir}/pylintrc blinkpy tests app
|
||||||
flake8 blinkpy tests
|
flake8 blinkpy tests app
|
||||||
pydocstyle blinkpy tests
|
pydocstyle blinkpy tests app
|
||||||
rst-lint README.rst
|
rst-lint README.rst
|
||||||
rst-lint CHANGES.rst
|
rst-lint CHANGES.rst
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user