Compare commits
330
Commits
v0.9.0
...
v0.15.0-rc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86aae07367 | ||
|
|
1e86e7eb79 | ||
|
|
0c7737cd7f | ||
|
|
0c79620baa | ||
|
|
9bc20cc82e | ||
|
|
c51551afc5 | ||
|
|
f80a93f8e1 | ||
|
|
853c596167 | ||
|
|
819e383b05 | ||
|
|
724734847a | ||
|
|
cdd01c5186 | ||
|
|
5a5c34b6a1 | ||
|
|
28271efbe6 | ||
|
|
1dbb8d4b21 | ||
|
|
e1bbbb5c7e | ||
|
|
addc32e3e6 | ||
|
|
b201c95e8d | ||
|
|
19b2541c81 | ||
|
|
6a2650dda0 | ||
|
|
c122c4ecfb | ||
|
|
43ed6f1600 | ||
|
|
fae1e1a554 | ||
|
|
4587268ecb | ||
|
|
e0ff5c484a | ||
|
|
bc4956c64b | ||
|
|
cc5fbcd3a7 | ||
|
|
db23acd46c | ||
|
|
973374f6a1 | ||
|
|
13e2262c2f | ||
|
|
69775188a6 | ||
|
|
0554e4e159 | ||
|
|
f576c97c1d | ||
|
|
e6c842072c | ||
|
|
bf87f3676d | ||
|
|
a87581638c | ||
|
|
42ed18fcd5 | ||
|
|
a66c926475 | ||
|
|
6a481aaa74 | ||
|
|
72d97f4944 | ||
|
|
1694187b39 | ||
|
|
a531a8e8a3 | ||
|
|
44d5bb65ae | ||
|
|
c64f5cfb23 | ||
|
|
19a29bc3a1 | ||
|
|
6159987928 | ||
|
|
8a8c4ea20b | ||
|
|
e43d2f9a88 | ||
|
|
0c3359abcf | ||
|
|
a76069174d | ||
|
|
19c9178db7 | ||
|
|
2ebf2db04d | ||
|
|
26de89c1b9 | ||
|
|
8f68aad238 | ||
|
|
7b586e2947 | ||
|
|
800ddeaf7a | ||
|
|
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 | ||
|
|
853ccbce79 | ||
|
|
8153fab887 | ||
|
|
216541288e | ||
|
|
a855cceb05 | ||
|
|
d9991d5acd | ||
|
|
2ee2ae7d4e | ||
|
|
04c2be9087 | ||
|
|
34fc9bc527 | ||
|
|
1a24e1896f | ||
|
|
1d67764dc8 | ||
|
|
8f250d204f | ||
|
|
643ae8c387 | ||
|
|
f9615a2d90 | ||
|
|
f7a3b1d0ef | ||
|
|
94aed43d2e | ||
|
|
90f30deea8 | ||
|
|
1bbc71ef17 | ||
|
|
c84a0deb5f | ||
|
|
ece65a9158 | ||
|
|
5a8af75a5b | ||
|
|
84f987d29c | ||
|
|
ed25745deb | ||
|
|
a1fd05735e | ||
|
|
5a9f8f6dfc | ||
|
|
2639055372 | ||
|
|
c53b2bd777 | ||
|
|
db29bae140 | ||
|
|
52f1fde47a | ||
|
|
e705fbd403 | ||
|
|
2d437b4462 | ||
|
|
ae946dbb53 | ||
|
|
ea2b22dbf0 | ||
|
|
7ee57b2c6b | ||
|
|
ccd393a533 | ||
|
|
1b571c7d4b | ||
|
|
4b91fcb615 | ||
|
|
1e0d069dcd | ||
|
|
92088779e0 | ||
|
|
a6e02433b6 | ||
|
|
1aa5dc1c71 | ||
|
|
3c78923853 | ||
|
|
d17183a152 | ||
|
|
2e5e343913 | ||
|
|
cec6035d23 | ||
|
|
bdc760608e | ||
|
|
4f41899338 | ||
|
|
557f21b637 | ||
|
|
4241b50002 | ||
|
|
8b0507f1a7 | ||
|
|
d62429189e | ||
|
|
c83b173a17 | ||
|
|
9c1974c227 | ||
|
|
c621ba3150 | ||
|
|
7710e3f972 | ||
|
|
a4f65c1102 | ||
|
|
80c6444c2e | ||
|
|
2022b80f9d | ||
|
|
7e9900c5f5 | ||
|
|
132df9e8c4 | ||
|
|
07e3549306 | ||
|
|
e0fa0a0cde | ||
|
|
31771ca87d | ||
|
|
1b898398a7 | ||
|
|
d8fcb679cd | ||
|
|
72a5ccb769 | ||
|
|
e872fc9ce5 | ||
|
|
51961474b6 | ||
|
|
8be35dab30 | ||
|
|
0ab17aede8 | ||
|
|
6db8a151f8 | ||
|
|
1a12eb367f | ||
|
|
f61f68a5c2 | ||
|
|
0781d79ca7 | ||
|
|
4ffdf499c9 | ||
|
|
f8f318c203 | ||
|
|
59540818f9 | ||
|
|
b2b531ad3c | ||
|
|
4e6957a772 | ||
|
|
e747fb1010 | ||
|
|
9d110cdd72 | ||
|
|
6fa0eacff3 | ||
|
|
7ce924f52e | ||
|
|
91b3c2a64b | ||
|
|
97579400b1 | ||
|
|
63b01dde14 | ||
|
|
d9b996ab02 | ||
|
|
3af105edf5 | ||
|
|
5057592b0d | ||
|
|
13bb819e07 | ||
|
|
eefa9252e5 | ||
|
|
c5b84ac357 | ||
|
|
787256ce66 | ||
|
|
2f1877267f | ||
|
|
9c34fc9281 | ||
|
|
1b9bba7c3d | ||
|
|
123867ecd8 | ||
|
|
26d900897b | ||
|
|
f7a5b5cc03 | ||
|
|
7fd822caf2 | ||
|
|
01c6f0b12b | ||
|
|
62c89a925e |
+3
-3
@@ -1,5 +1,5 @@
|
||||
[run]
|
||||
omit =
|
||||
helpers/*
|
||||
parallel = true
|
||||
omit =
|
||||
tests/*
|
||||
setup.py
|
||||
setup.py
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Home Assistant version (if applicable):** <HA Version Number>
|
||||
|
||||
**`blinkpy` version (not needed if filling out Home Assistant version):**
|
||||
|
||||
**Log Output/Additional Information**
|
||||
If using home-assistant, please paste the output of the log showing your error below. If not, please include any additional useful information.
|
||||
|
||||
```
|
||||
PASTE LOG OUTPUT HERE
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the feature request (such as API endpoint responses, etc).
|
||||
@@ -5,5 +5,5 @@
|
||||
|
||||
## Checklist:
|
||||
- [ ] Local tests with `tox` run successfully **PR cannot be meged unless tests pass**
|
||||
- [ ] If user-facing functionality changed, README.rst updated
|
||||
- [ ] Tests added to verify new code works
|
||||
- [ ] Changes tested locally to ensure platform still works as intended
|
||||
- [ ] Tests added to verify new code works
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- Priority
|
||||
- bug
|
||||
- Help Wanted
|
||||
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: true
|
||||
|
||||
# Set to true to ignore issues in a milestone (defaults to false)
|
||||
exemptMilestones: true
|
||||
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs within seven days. Thank you
|
||||
for your contributions.
|
||||
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: >
|
||||
This issue is now being closed due to inactivity.
|
||||
@@ -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: 3
|
||||
matrix:
|
||||
platform:
|
||||
- ubuntu-latest
|
||||
python-version: [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
|
||||
+3
-1
@@ -3,10 +3,12 @@
|
||||
.tox/*
|
||||
__pycache__/*
|
||||
htmlcov/*
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage.xml
|
||||
*.pyc
|
||||
*.egg*/*
|
||||
dist/*
|
||||
.sh
|
||||
build/*
|
||||
docs/_build
|
||||
*.log
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
python:
|
||||
enabled: true
|
||||
ignore_file: setup.py
|
||||
ignored_directories:
|
||||
- docs
|
||||
|
||||
fail_on_violations: true
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
sudo: required
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- python: "3.6"
|
||||
env: TOXENV=lint
|
||||
- python: "3.5"
|
||||
env: TOXENV=py35
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
- python: "3.6"
|
||||
env: TOXENV=build
|
||||
- python: "3.7"
|
||||
env: TOXENV=py37
|
||||
dist: xenial
|
||||
|
||||
install: pip install -U tox coveralls
|
||||
language: python
|
||||
script: tox
|
||||
after_success: coveralls
|
||||
@@ -1,5 +1,5 @@
|
||||
# BlinkMonitorProtocol
|
||||
Unofficial documentation for the Client API of the Blink Wire-Free HD Home Monitoring and Alert System.
|
||||
Unofficial documentation for the Client API of the Blink Wire-Free HD Home Monitoring & Alert System.
|
||||
|
||||
Copied from https://github.com/MattTW/BlinkMonitorProtocol
|
||||
|
||||
@@ -7,9 +7,6 @@ I am not affiliated with the company in any way - this documentation is strictly
|
||||
|
||||
The Client API is a straightforward REST API using JSON and HTTPS.
|
||||
|
||||
## This Document
|
||||
The purpose here is to describe what is going on behind the scenes, and what commands we know are available to communicate with Blink servers. This is NOT a description of the blinkpy module, but a description of the commands blinkpy relies on to effectively communicate.
|
||||
|
||||
## Login
|
||||
|
||||
Client login to the Blink Servers.
|
||||
@@ -19,20 +16,25 @@ Client login to the Blink Servers.
|
||||
> "password" : "*your blink password*",
|
||||
> "client_specifier" : "iPhone 9.2 | 2.2 | 222",
|
||||
> "email" : "*your blink login/email*"
|
||||
>}' --compressed https://prod.immedia-semi.com/login
|
||||
>}' --compressed https://rest.prod.immedia-semi.com/login
|
||||
|
||||
**Response:**
|
||||
>{"authtoken":{"authtoken":"*an auth token*","message":"auth"}}
|
||||
>{"authtoken":{"authtoken":"*an auth token*","message":"auth"},"networks":{"*network id*":{"name":"*name*","onboarded":true}},"region":{"*regioncode for endpoint*":"*region name"}}
|
||||
|
||||
**Notes:**
|
||||
The authtoken value is passed in a header in future calls.
|
||||
The region code for endpoint is required to form the URL of the REST endpoint for future calls.
|
||||
Depending on the region you are registered you will need to change the REST endpoints below:
|
||||
- from `https://rest.prod.immedia-semi.com`
|
||||
- to `https://rest.prde.immedia-semi.com` if e.g. your device is registered in Germany
|
||||
Please note that at this moment it seems that all regions are not implemented equally: not all endpoints are available in all regions
|
||||
|
||||
##Networks
|
||||
## Networks
|
||||
|
||||
Obtain information about the Blink networks defined for the logged in user.
|
||||
|
||||
**Request:**
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/networks
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/networks
|
||||
|
||||
**Response:**
|
||||
JSON response containing information including Network ID and Account ID.
|
||||
@@ -46,7 +48,7 @@ Network ID is needed to issue arm/disarm calls
|
||||
Obtain information about the Blink Sync Modules on the given network.
|
||||
|
||||
**Request:**
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id_from_networks_call*/syncmodules
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id_from_networks_call*/syncmodules
|
||||
|
||||
**Response:**
|
||||
JSON response containing information about the known state of the Sync module, most notably if it is online
|
||||
@@ -56,11 +58,11 @@ Probably not strictly needed but checking result can verify that the sync module
|
||||
|
||||
|
||||
## Arm
|
||||
|
||||
|
||||
Arm the given network (start recording/reporting motion events)
|
||||
|
||||
**Request:**
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id_from_networks_call*/arm
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id_from_networks_call*/arm
|
||||
|
||||
**Response:**
|
||||
JSON response containing information about the arm command request, including the command/request ID
|
||||
@@ -73,7 +75,7 @@ When this call returns, it does not mean the arm request is complete, the clien
|
||||
Disarm the given network (stop recording/reporting motion events)
|
||||
|
||||
**Request:**
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id_from_networks_call*/disarm
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id_from_networks_call*/disarm
|
||||
|
||||
**Response:**
|
||||
JSON response containing information about the disarm command request, including the command/request ID
|
||||
@@ -87,7 +89,7 @@ When this call returns, it does not mean the disarm request is complete, the cl
|
||||
Get status info on the given command
|
||||
|
||||
**Request:**
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/command/*command_id*
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/command/*command_id*
|
||||
|
||||
**Response:**
|
||||
JSON response containing state information of the given command, most notably whether it has completed and was successful.
|
||||
@@ -103,7 +105,7 @@ lv_relay, arm, disarm, thumbnail, clip
|
||||
Return information displayed on the home screen of the mobile client
|
||||
|
||||
**Request:**
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/homescreen
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/homescreen
|
||||
|
||||
**Response:**
|
||||
JSON response containing information that the mobile client displays on the home page, including: status, armed state, links to thumbnails for each camera, etc.
|
||||
@@ -111,12 +113,12 @@ JSON response containing information that the mobile client displays on the home
|
||||
**Notes:**
|
||||
Not necessary to as part of issuing arm/disarm commands, but contains good summary info.
|
||||
|
||||
##Events, thumbnails & video captures
|
||||
## Events, thumbnails & video captures
|
||||
|
||||
**Request**
|
||||
Get events for a given network (sync module) -- Need network ID from home
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/events/network/*network__id*
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/events/network/*network__id*
|
||||
|
||||
**Response**
|
||||
A json list of evets incluing URL's. Replace the "mp4" with "jpg" extension to get the thumbnail of each clip
|
||||
@@ -144,7 +146,7 @@ Note that you replace the 'mp4' with a 'jpg' to get the thumbnail
|
||||
**Request**
|
||||
Captures a new thumbnail for a camera
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/thumbnail
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/thumbnail
|
||||
|
||||
**Response**
|
||||
Command information.
|
||||
@@ -152,17 +154,17 @@ Command information.
|
||||
**Request**
|
||||
Captures a new video for a camera
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/clip
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/clip
|
||||
|
||||
**Response**
|
||||
Command information.
|
||||
|
||||
##Video Information
|
||||
## Video Information
|
||||
|
||||
**Request**
|
||||
Get the total number of videos in the system
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/videos/count
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/videos/count
|
||||
|
||||
**Response**
|
||||
JSON response containing the total video count.
|
||||
@@ -170,7 +172,7 @@ JSON response containing the total video count.
|
||||
**Request**
|
||||
Gets a paginated set of video information
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/videos/page/0
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/videos/page/0
|
||||
|
||||
**Response**
|
||||
JSON response containing a set of video information, including: camera name, creation time, thumbnail URI, size, length
|
||||
@@ -178,7 +180,7 @@ JSON response containing a set of video information, including: camera name, cre
|
||||
**Request**
|
||||
Gets information for a specific video by ID
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/video/*video_id*
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/video/*video_id*
|
||||
|
||||
**Response**
|
||||
JSON response containing video information
|
||||
@@ -186,7 +188,7 @@ JSON response containing video information
|
||||
**Request**
|
||||
Gets a list of unwatched videos
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/videos/unwatched
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/videos/unwatched
|
||||
|
||||
**Response**
|
||||
JSON response containing unwatched video information
|
||||
@@ -194,7 +196,7 @@ JSON response containing unwatched video information
|
||||
**Request**
|
||||
Deletes a video
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/api/v2/video/*video_id*/delete
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/api/v2/video/*video_id*/delete
|
||||
|
||||
**Response**
|
||||
Unknown - not tested
|
||||
@@ -202,7 +204,7 @@ Unknown - not tested
|
||||
**Request**
|
||||
Deletes all videos
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/api/v2/videos/deleteall
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/api/v2/videos/deleteall
|
||||
|
||||
**Response**
|
||||
Unknown - not tested
|
||||
@@ -212,7 +214,7 @@ Unknown - not tested
|
||||
**Request**
|
||||
Gets a list of cameras
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/cameras
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/cameras
|
||||
|
||||
**Response**
|
||||
JSON response containing camera information
|
||||
@@ -220,7 +222,7 @@ JSON response containing camera information
|
||||
**Request**
|
||||
Gets information for one camera
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*
|
||||
|
||||
**Response**
|
||||
JSON response containing camera information
|
||||
@@ -228,7 +230,7 @@ JSON response containing camera information
|
||||
**Request**
|
||||
Gets camera sensor information
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/signals
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/signals
|
||||
|
||||
**Response**
|
||||
JSON response containing camera sensor information, such as wifi strength, temperature, and battery level
|
||||
@@ -236,7 +238,7 @@ JSON response containing camera sensor information, such as wifi strength, tempe
|
||||
**Request**
|
||||
Enables motion detection for one camera
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/enable
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/enable
|
||||
|
||||
**Response**
|
||||
JSON response containing camera information
|
||||
@@ -244,7 +246,7 @@ JSON response containing camera information
|
||||
**Request**
|
||||
Disables motion detection for one camera
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/disable
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/disable
|
||||
|
||||
**Response**
|
||||
JSON response containing camera information
|
||||
@@ -257,7 +259,7 @@ JSON response containing camera information
|
||||
**Request**
|
||||
Gets information about devices that have connected to the blink service
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/account/clients
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/account/clients
|
||||
|
||||
**Response**
|
||||
JSON response containing client information, including: type, name, connection time, user ID
|
||||
@@ -265,7 +267,7 @@ JSON response containing client information, including: type, name, connection t
|
||||
**Request**
|
||||
Gets information about supported regions
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/regions
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/regions
|
||||
|
||||
**Response**
|
||||
JSON response containing region information
|
||||
@@ -273,7 +275,7 @@ JSON response containing region information
|
||||
**Request**
|
||||
Gets information about system health
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/health
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/health
|
||||
|
||||
**Response**
|
||||
"all ports tested are open"
|
||||
@@ -281,7 +283,7 @@ Gets information about system health
|
||||
**Request**
|
||||
Gets information about programs
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v1/networks/*network_id*/programs
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v1/networks/*network_id*/programs
|
||||
|
||||
**Response**
|
||||
Unknown.
|
||||
|
||||
+160
-8
@@ -3,31 +3,183 @@ Changelog
|
||||
|
||||
A list of changes between each release
|
||||
|
||||
0.9.0.dev (Development Version)
|
||||
0.14.3 (2020-04-22)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Add time check on recorded videos before determining motion
|
||||
- Fix motion detection variable suck to ``True``
|
||||
- Add ability to load credentials from a json file
|
||||
- Only allow ``motion_detected`` variable to trigger if system was armed
|
||||
- Log response message from server if not attempting a re-authorization
|
||||
|
||||
0.14.2 (2019-10-12)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- 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)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Fixed incorrect backup login url
|
||||
- Added calibrated temperature property for cameras
|
||||
|
||||
|
||||
0.11.0 (2018-11-23)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Added support for multiple sync modules
|
||||
|
||||
0.10.3 (2018-11-18)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Use networks endpoint rather than homecreen to retrieve arm/disarm status (`@md-reddevil <https://github.com/fronzbot/blinkpy/pull/119>`__)
|
||||
- Fix incorrect command status endpoint (`@md-reddevil <https://github.com/fronzbot/blinkpy/pull/118>`__)
|
||||
- Add extra debug logging
|
||||
- Remove error prior to re-authorization (only log error when re-auth failed)
|
||||
|
||||
|
||||
0.10.2 (2018-10-30)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Set minimum required version of the requests library to 2.20.0 due to vulnerability in earlier releases.
|
||||
- When multiple networks detected, changed log level to ``warning`` from ``error``
|
||||
|
||||
|
||||
0.10.1 (2018-10-18)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Fix re-authorization bug (fixes `#101 <https://github.com/fronzbot/blinkpy/issues/#101>`__)
|
||||
- Log an error if saving video that doesn't exist
|
||||
|
||||
0.10.0 (2018-10-16)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Moved all API calls to own module for easier maintainability
|
||||
- Added network ids to sync module and cameras to allow for multi-network use
|
||||
- Removed dependency on video existance prior to camera setup (fixes `#93 <https://github.com/fronzbot/blinkpy/issues/#93>`__)
|
||||
- Camera wifi_strength now reported in wifi "bars" rather than dBm due to API endpoint change
|
||||
- Use homescreen thumbnail as fallback in case it's not in the camera endpoint
|
||||
- Removed "armed" and "status" attributes from camera (status of camera only reported by "motion_enabled" now)
|
||||
- Added serial number attributes to sync module and cameras
|
||||
- Check network_id from login response and verify that network is onboarded (fixes `#90 <https://github.com/fronzbot/#90>`__)
|
||||
- Check if retrieved clip is "None" prior to storing in cache
|
||||
|
||||
0.9.0 (2018-09-27)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Complete code refactoring to enable future multi-sync module support
|
||||
- Add image and video caching to the cameras
|
||||
- Add internal throttling of system refresh
|
||||
- Use session for http requests
|
||||
|
||||
**Breaking change:**
|
||||
- Cameras now accessed through sync module ``Blink.sync.cameras``
|
||||
|
||||
|
||||
0.8.1 (2018-09-24)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Update requirements_test.txt
|
||||
- Update linter versions
|
||||
- Fix pylint warnings
|
||||
- Remove object from class declarations
|
||||
- Remove useless returns from functions
|
||||
- Fix pylint errors
|
||||
- change if comparison to fix (consider-using-in)
|
||||
- Disabled no else-if-return check
|
||||
- Fix useless-import-alias
|
||||
- Disable no-else-return
|
||||
- Fix motion detection
|
||||
- Use an array of recent video clips to determine if motion has been detected.
|
||||
- Reset the value every system refresh
|
||||
|
||||
0.8.0 (2018-05-21)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Added support for battery voltage level (fixes `#64 <https://github.com/fronzbot/blinkpy/issues/64>`_)
|
||||
- Added support for battery voltage level (fixes `#64 <https://github.com/fronzbot/blinkpy/issues/64>`__)
|
||||
- Added motion detection per camera
|
||||
- Added fully accessible camera configuration dict
|
||||
- Added celcius property to camera (fixes `#60 <https://github.com/fronzbot/blinkpy/issues/60>`_)
|
||||
- Added celcius property to camera (fixes `#60 <https://github.com/fronzbot/blinkpy/issues/60>`__)
|
||||
|
||||
0.7.1 (2018-05-09)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Fixed pip 10 import issue during setup (`@fronzbot <https://github.com/fronzbot/blinkpy/pull/61>`_)
|
||||
- Fixed pip 10 import issue during setup (`@fronzbot <https://github.com/fronzbot/blinkpy/pull/61>`__)
|
||||
|
||||
0.7.0 (2018-02-08)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Fixed style errors for bumped pydocstring and pylint versions
|
||||
- Changed Blink.cameras dictionary to be case-insensitive (fixes `#35 <https://github.com/fronzbot/blinkpy/issues/35>`_)
|
||||
- Changed api endpoint for video extraction (fixes `#35 <https://github.com/fronzbot/blinkpy/issues/35>`_ and `#41 <https://github.com/fronzbot/blinkpy/issues/41>`_)
|
||||
- Changed Blink.cameras dictionary to be case-insensitive (fixes `#35 <https://github.com/fronzbot/blinkpy/issues/35>`__)
|
||||
- Changed api endpoint for video extraction (fixes `#35 <https://github.com/fronzbot/blinkpy/issues/35>`__ and `#41 <https://github.com/fronzbot/blinkpy/issues/41>`__)
|
||||
- Removed last_motion() function from Blink class
|
||||
- Refactored code for better organization
|
||||
- Moved some request calls out of @property methods (enables future CLI support)
|
||||
- Renamed get_summary() method to summary and changed to @property
|
||||
- Added ability to download most recent video clip
|
||||
- Improved camera arm/disarm handling (`@b10m <https://github.com/fronzbot/blinkpy/pull/50>`_)
|
||||
- Improved camera arm/disarm handling (`@b10m <https://github.com/fronzbot/blinkpy/pull/50>`__)
|
||||
- Added authentication to ``login()`` function and deprecated ``setup_system()`` in favor of ``start()``
|
||||
- Added ``attributes`` dictionary to camera object
|
||||
|
||||
@@ -36,7 +188,7 @@ A list of changes between each release
|
||||
- Removed redundent properties that only called hidden variables
|
||||
- Revised request wrapper function to be more intelligent
|
||||
- Added tests to ensure exceptions are caught and handled (100% coverage!)
|
||||
- Added auto-reauthorization (token refresh) when a request fails due to an expired token (`@tySwift93 <https://github.com/fronzbot/blinkpy/pull/24>`_)
|
||||
- Added auto-reauthorization (token refresh) when a request fails due to an expired token (`@tySwift93 <https://github.com/fronzbot/blinkpy/pull/24>`__)
|
||||
- Added battery level string to reduce confusion with the way Blink reports battery level as integer from 0 to 3
|
||||
|
||||
0.5.2 (2017-03-12)
|
||||
|
||||
+41
-35
@@ -4,41 +4,55 @@ Everyone is welcome to contribute to blinkpy! The process to get started is desc
|
||||
|
||||
## Fork the Repository
|
||||
|
||||
You can do this right in gituhb: just click the 'fork' button at the top right.
|
||||
You can do this right in github: just click the 'fork' button at the top right.
|
||||
|
||||
## Setup Local Repository
|
||||
## Start Developing
|
||||
|
||||
```shell
|
||||
$ git clone https://github.com/<YOUR_GIT_USERNAME>/blinkpy.git
|
||||
$ cd blinkpy
|
||||
$ git remote add upstream https://github.com/fronzbot/blinkpy.git
|
||||
```
|
||||
1. Setup Local Repository
|
||||
```shell
|
||||
$ git clone https://github.com/<YOUR_GIT_USERNAME>/blinkpy.git
|
||||
$ cd blinkpy
|
||||
$ git remote add upstream https://github.com/fronzbot/blinkpy.git
|
||||
```
|
||||
|
||||
## Create a Local Branch
|
||||
2. Create a Local Branch
|
||||
|
||||
First, you will want to create a new branch to hold your changes:
|
||||
``git checkout -b <your-branch-name>``
|
||||
|
||||
First, you will want to create a new branch to hold your changes:
|
||||
``git checkout -b <your-branch-name>``
|
||||
Next, you need to make sure you pull from the 'dev' branch:
|
||||
``git pull origin dev``
|
||||
|
||||
## Make changes
|
||||
3. Make changes
|
||||
|
||||
Now you can make changes to your code. It is worthwhile to test your code as you progress (see the **Testing** section)
|
||||
|
||||
Now you can make changes to your code. It is worthwhile to test your code as you progress (see the **Testing** section)
|
||||
4. Commit Your Changes
|
||||
|
||||
To commit changes to your branch, simply add the files you want and the commit them to the branch. After that, you can push to your fork on GitHub:
|
||||
```shell
|
||||
$ git add .
|
||||
$ git commit -m "Put your commit text here. Please be concise, but descriptive."
|
||||
$ git push origin HEAD
|
||||
```
|
||||
|
||||
5. Submit your pull request on GitHub
|
||||
|
||||
- On GitHub, navigate to the [blinkpy](https://github.com/fronzbot/blinkpy) repository.
|
||||
- In the "Branch" menu, choose the branch that contains your commits (from your fork).
|
||||
- To the right of the Branch menu, click New pull request.
|
||||
- The base branch dropdown menu should read `dev`. Use the compare branch drop-down menu to choose the branch you made your changes in.
|
||||
- Type a title and complete the provided description for your pull request.
|
||||
- Click Create pull request.
|
||||
- More detailed instructions can be found here: [Creating a Pull Request](https://help.github.com/articles/creating-a-pull-request/)
|
||||
|
||||
6. Prior to merge approval
|
||||
|
||||
Finally, the `blinkpy` repository uses continuous integration tools to run tests prior to merging. If there are any problems, you will see a red 'X' next to your pull request. To see what's wrong, you can find your pull request [here](https://travis- ci.org/fronzbot/blinkpy/pull_requests) and click on the failing test to see the logs. Those logs will indicate, as best as they can, what is causing that test to fail.
|
||||
|
||||
## Commit Your Changes
|
||||
|
||||
To commit changes to your branch, simply add the files you want and the commit them to the branch. After that, you can push to your fork on GitHub:
|
||||
|
||||
```shell
|
||||
$ git add .
|
||||
$ git commit -m "Put your commit text here. Please be concise, but descriptive."
|
||||
$ git push origin HEAD
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
It is important to test the code to make sure your changes don't break anything major and that they pass PEP8 style conventions.
|
||||
FIrst, you need to locally install ``tox``
|
||||
First, you need to locally install ``tox``
|
||||
|
||||
```shell
|
||||
$ pip3 install tox
|
||||
@@ -52,11 +66,11 @@ $ tox
|
||||
|
||||
### Tips
|
||||
|
||||
If you only want to see if you can pass the local tests, you can run ``tox -e py34``. If you just want to check for style violations, you can run ``tox -e lint``. Regardless, when you submit a pull request, your code MUST pass both the unit tests, and the linters.
|
||||
If you only want to see if you can pass the local tests, you can run `tox -e py35` (or whatever python version you have installed. Only `py35`, `py36`, and `py37` will be accepted). If you just want to check for style violations, you can run `tox -e lint`. Regardless, when you submit a pull request, your code MUST pass both the unit tests, and the linters.
|
||||
|
||||
If you need to change anything in ``requirements.txt`` for any reason, you'll want to regenerate the virtual envrionments used by ``tox`` by running with the ``-r`` flag: ``tox -r``
|
||||
If you need to change anything in `requirements.txt` for any reason, you'll want to regenerate the virtual envrionments used by `tox` by running with the `-r` flag: `tox -r`
|
||||
|
||||
Please do not locally disable any linter warnings within the ``blinkpy.py`` module itself (it's ok to do this in any of the ``test_*.py`` files)
|
||||
If you want to run a single test (perhaps you only changed a small thing in one file) you can run `tox -e py35 -- tests/<testname>.py -x`. This will run the test `<testname>.py` and stop testing upon the first failure, making it easier to figure out why a particular test might be failing. The test structure mimics the library structure, so if you changed something in `sync_module.py`, the associated test file would be in `test_sync_module.py` (ie. the filename is prepended with `test_`.
|
||||
|
||||
# Catching Up With Reality
|
||||
|
||||
@@ -73,11 +87,3 @@ If rebase detects conflicts, repeat the following process until all changes have
|
||||
2. Add the modified file: ``git add <file>`` or ``git add .``.
|
||||
3. Continue rebase: ``git rebase --continue``.
|
||||
4. Repeat until all conflicts resolved.
|
||||
|
||||
# Creating a Pull Request
|
||||
|
||||
Please follow these steps to create a pull request against the ``dev`` branch: [Creating a Pull Request](https://help.github.com/articles/creating-a-pull-request/)
|
||||
|
||||
# Monitor Build Status
|
||||
|
||||
Once you create your PR, you can monitor the status of your build [here](https://travis-ci.org/fronzbot/blinkpy), Your code will be tested to ensure it passes and won't cause any problems after merging.
|
||||
+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 []
|
||||
+64
-17
@@ -1,7 +1,10 @@
|
||||
blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version|
|
||||
================================================================
|
||||
blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version| |Python Version| |Codestyle|
|
||||
=============================================================================================
|
||||
A Python library for the Blink Camera system
|
||||
Only compatible with Python 3+
|
||||
|
||||
Like the library? Consider buying me a cup of coffee!
|
||||
|
||||
|Donate|
|
||||
|
||||
Disclaimer:
|
||||
~~~~~~~~~~~~~~~
|
||||
@@ -32,12 +35,16 @@ To install the current development version, perform the following steps. Note t
|
||||
$ pip3 install --upgrade dist/*.whl
|
||||
|
||||
|
||||
If you'd like to contribute to this library, please read the `contributing instructions <https://github.com/fronzbot/blinkpy/blob/dev/CONTRIBUTING.md>`__.
|
||||
|
||||
For more information on how to use this library, please `read the docs <https://blinkpy.readthedocs.io/en/latest/>`__.
|
||||
|
||||
Purpose
|
||||
===========
|
||||
This library was built with the intention of allowing easy communication with Blink camera systems, specifically so I can add a module into homeassistant https://home-assistant.io
|
||||
This library was built with the intention of allowing easy communication with Blink camera systems, specifically to support the `Blink component <https://home-assistant.io/components/blink>`__ in `homeassistant <https://home-assistant.io/>`__.
|
||||
|
||||
Usage
|
||||
=========
|
||||
Quick Start
|
||||
=============
|
||||
The simplest way to use this package from a terminal is to call ``Blink.start()`` which will prompt for your Blink username and password and then log you in. Alternatively, you can instantiate the Blink class with a username and password, and call ``Blink.start()`` to login and setup without prompt, as shown below. In addition, http requests are throttled internally via use of the ``Blink.refresh_rate`` variable, which can be set at initialization and defaults to 30 seconds.
|
||||
|
||||
.. code:: python
|
||||
@@ -46,9 +53,31 @@ The simplest way to use this package from a terminal is to call ``Blink.start()`
|
||||
blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD', refresh_rate=30)
|
||||
blink.start()
|
||||
|
||||
If you would like to log in without setting up the cameras or system, you can simply call the ``Blink.login()`` function which will prompt for a username and password and then authenticate with the server. This is useful if you want to avoid use of the ``start()`` function which simply acts as a wrapper for more targeted API methods.
|
||||
At startup, you may ne prompted for a verification key. Just enter this in the command-line prompt. If you just receive a verification email asking to validate access for your device, enter nothing at this prompt. To avoid any command-line interaction, call the ``Blink`` class with the ``no_prompt=True`` flag. Instead, once you receive the verification email, call the following functions:
|
||||
|
||||
Cameras are instantiated as individual ``BlinkCamera`` classes within a ``BlinkSyncModule`` instance. Note: currently the API only supports one sync module, but multiple sync modules are planned to be supported in the future.
|
||||
.. code:: python
|
||||
|
||||
blink.login_handler.send_auth_key(blink, VERIFICATION_KEY)
|
||||
blink.setup_post_verify()
|
||||
|
||||
In addition, you can also save your credentials in a json file and initialize Blink with the credential file as follows:
|
||||
|
||||
.. 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).
|
||||
|
||||
The below code will display cameras and their available attributes:
|
||||
|
||||
@@ -59,15 +88,15 @@ The below code will display cameras and their available attributes:
|
||||
blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD')
|
||||
blink.start()
|
||||
|
||||
for name, camera in blink.sync.cameras.items():
|
||||
print(name) # Name of the camera
|
||||
print(camera.attributes) # Print available attributes of camera
|
||||
for name, camera in blink.cameras.items():
|
||||
print(name) # Name of the camera
|
||||
print(camera.attributes) # Print available attributes of camera
|
||||
|
||||
The most recent images and videos can be accessed as a bytes-object via internal variables. These can be updated with calls to ``Blink.refresh()`` but will only make a request if motion has been detected or other changes have been found. This can be overridden with the ``force_cache`` flag, but this should be used for debugging only since it overrides the internal request throttling.
|
||||
|
||||
.. code:: python
|
||||
|
||||
camera = blink.sync.camera['SOME CAMERA NAME']
|
||||
camera = blink.cameras['SOME CAMERA NAME']
|
||||
blink.refresh(force_cache=True) # force a cache update USE WITH CAUTION
|
||||
camera.image_from_cache.raw # bytes-like image object (jpg)
|
||||
camera.video_from_cache.raw # bytes-like video object (mp4)
|
||||
@@ -76,17 +105,35 @@ The ``blinkpy`` api also allows for saving images and videos to a file and snapp
|
||||
|
||||
.. code:: python
|
||||
|
||||
camera = blink.sync.camera['SOME CAMERA NAME']
|
||||
camera = blink.cameras['SOME CAMERA NAME']
|
||||
camera.snap_picture() # Take a new picture with the camera
|
||||
blink.refresh() # Get new information from server
|
||||
camera.image_to_file('/local/path/for/image.jpg')
|
||||
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
|
||||
:target: https://travis-ci.org/fronzbot/blinkpy
|
||||
.. |Coverage Status| image:: https://coveralls.io/repos/github/fronzbot/blinkpy/badge.svg?branch=dev
|
||||
:target: https://coveralls.io/github/fronzbot/blinkpy?branch=dev
|
||||
Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34am to the ``/home/blink`` directory:
|
||||
|
||||
.. code:: python
|
||||
|
||||
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
|
||||
:target: https://pypi.python.org/pypi/blinkpy
|
||||
.. |Docs| image:: https://readthedocs.org/projects/blinkpy/badge/?version=latest
|
||||
:target: http://blinkpy.readthedocs.io/en/latest/?badge=latest
|
||||
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/blinkpy.svg
|
||||
:target: https://img.shields.io/pypi/pyversions/blinkpy.svg
|
||||
|
||||
.. |Donate| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif
|
||||
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UR6Z2B8GXYUCC
|
||||
.. |Codestyle| image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/psf/black
|
||||
|
||||
@@ -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
|
||||
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
"""Implements known blink API calls."""
|
||||
|
||||
import logging
|
||||
from json import dumps
|
||||
import blinkpy.helpers.errors as ERROR
|
||||
from blinkpy.helpers.util import http_req, get_time, BlinkException, Throttle
|
||||
from blinkpy.helpers.constants import DEFAULT_URL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_THROTTLE_TIME = 2
|
||||
|
||||
|
||||
def request_login(
|
||||
blink, url, username, password, notification_key, uid, is_retry=False
|
||||
):
|
||||
"""
|
||||
Login request.
|
||||
|
||||
:param blink: Blink instance.
|
||||
:param url: Login url.
|
||||
:param username: Blink username.
|
||||
:param password: Blink password.
|
||||
:param notification_key: Randomly genereated key.
|
||||
:param uid: Randomoly genreated unique id key.
|
||||
:param is_retry: Is this part of a re-authorization attempt?
|
||||
"""
|
||||
headers = {"Host": DEFAULT_URL, "Content-Type": "application/json"}
|
||||
data = dumps(
|
||||
{
|
||||
"email": username,
|
||||
"password": password,
|
||||
"notification_key": notification_key,
|
||||
"unique_id": uid,
|
||||
"app_version": "6.0.7 (520300) #afb0be72a",
|
||||
"client_name": "Computer",
|
||||
"client_type": "android",
|
||||
"device_identifier": "Blinkpy",
|
||||
"device_name": "Blinkpy",
|
||||
"os_version": "5.1.1",
|
||||
"reauth": "true",
|
||||
}
|
||||
)
|
||||
return http_req(
|
||||
blink,
|
||||
url=url,
|
||||
headers=headers,
|
||||
data=data,
|
||||
json_resp=False,
|
||||
reqtype="post",
|
||||
is_retry=is_retry,
|
||||
)
|
||||
|
||||
|
||||
def request_verify(blink, verify_key):
|
||||
"""Send verification key to blink servers."""
|
||||
url = "{}/api/v4/account/{}/client/{}/pin/verify".format(
|
||||
blink.urls.base_url, blink.account_id, blink.client_id
|
||||
)
|
||||
data = dumps({"pin": verify_key})
|
||||
return http_req(
|
||||
blink,
|
||||
url=url,
|
||||
headers=blink.auth_header,
|
||||
data=data,
|
||||
json_resp=False,
|
||||
reqtype="post",
|
||||
)
|
||||
|
||||
|
||||
def request_networks(blink):
|
||||
"""Request all networks information."""
|
||||
url = "{}/networks".format(blink.urls.base_url)
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
def request_network_status(blink, network):
|
||||
"""
|
||||
Request network information.
|
||||
|
||||
:param blink: Blink instance.
|
||||
:param network: Sync module network id.
|
||||
"""
|
||||
url = "{}/network/{}".format(blink.urls.base_url, network)
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
def request_syncmodule(blink, network):
|
||||
"""
|
||||
Request sync module info.
|
||||
|
||||
:param blink: Blink instance.
|
||||
:param network: Sync module network id.
|
||||
"""
|
||||
url = "{}/network/{}/syncmodules".format(blink.urls.base_url, network)
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def request_system_arm(blink, network):
|
||||
"""
|
||||
Arm system.
|
||||
|
||||
:param blink: Blink instance.
|
||||
:param network: Sync module network id.
|
||||
"""
|
||||
url = "{}/network/{}/arm".format(blink.urls.base_url, network)
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def request_system_disarm(blink, network):
|
||||
"""
|
||||
Disarm system.
|
||||
|
||||
:param blink: Blink instance.
|
||||
:param network: Sync module network id.
|
||||
"""
|
||||
url = "{}/network/{}/disarm".format(blink.urls.base_url, network)
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
def request_command_status(blink, network, command_id):
|
||||
"""
|
||||
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, network, command_id)
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def request_homescreen(blink):
|
||||
"""Request homescreen info."""
|
||||
url = "{}/api/v3/accounts/{}/homescreen".format(
|
||||
blink.urls.base_url, blink.account_id
|
||||
)
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def request_sync_events(blink, network):
|
||||
"""
|
||||
Request events from sync module.
|
||||
|
||||
:param blink: Blink instance.
|
||||
:param network: Sync module network id.
|
||||
"""
|
||||
url = "{}/events/network/{}".format(blink.urls.base_url, network)
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def request_new_image(blink, network, camera_id):
|
||||
"""
|
||||
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, network, camera_id
|
||||
)
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def request_new_video(blink, network, camera_id):
|
||||
"""
|
||||
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, network, camera_id)
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def request_video_count(blink):
|
||||
"""Request total video count."""
|
||||
url = "{}/api/v2/videos/count".format(blink.urls.base_url)
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
def request_videos(blink, time=None, page=0):
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
def request_cameras(blink, network):
|
||||
"""
|
||||
Request all camera information.
|
||||
|
||||
:param Blink: Blink instance.
|
||||
:param network: Sync module network id.
|
||||
"""
|
||||
url = "{}/network/{}/cameras".format(blink.urls.base_url, network)
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
def request_camera_info(blink, network, camera_id):
|
||||
"""
|
||||
Request camera info for one camera.
|
||||
|
||||
: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)
|
||||
|
||||
|
||||
def request_camera_sensors(blink, network, camera_id):
|
||||
"""
|
||||
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, network, camera_id
|
||||
)
|
||||
return http_get(blink, url)
|
||||
|
||||
|
||||
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def request_motion_detection_enable(blink, network, camera_id):
|
||||
"""
|
||||
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, network, camera_id
|
||||
)
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
@Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def request_motion_detection_disable(blink, network, camera_id):
|
||||
"""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, network, camera_id
|
||||
)
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
def http_get(blink, url, stream=False, json=True, is_retry=False):
|
||||
"""
|
||||
Perform an http get request.
|
||||
|
||||
:param url: URL to perform get request.
|
||||
:param stream: Stream 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:
|
||||
raise BlinkException(ERROR.AUTH_TOKEN)
|
||||
_LOGGER.debug("Making GET request to %s", url)
|
||||
return http_req(
|
||||
blink,
|
||||
url=url,
|
||||
headers=blink.auth_header,
|
||||
reqtype="get",
|
||||
stream=stream,
|
||||
json_resp=json,
|
||||
is_retry=is_retry,
|
||||
)
|
||||
|
||||
|
||||
def http_post(blink, url, is_retry=False):
|
||||
"""
|
||||
Perform an http 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:
|
||||
raise BlinkException(ERROR.AUTH_TOKEN)
|
||||
_LOGGER.debug("Making POST request to %s", url)
|
||||
return http_req(
|
||||
blink, url=url, headers=blink.auth_header, reqtype="post", is_retry=is_retry
|
||||
)
|
||||
+259
-120
@@ -1,70 +1,122 @@
|
||||
|
||||
# -*- 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 :
|
||||
https://github.com/MattTW/BlinkMonitorProtocol
|
||||
|
||||
Published under the MIT license - See LICENSE file for more details.
|
||||
"Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark
|
||||
owned by Immedia Inc., see www.blinkforhome.com for more information.
|
||||
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 json
|
||||
import getpass
|
||||
import logging
|
||||
import blinkpy.helpers.errors as ERROR
|
||||
from shutil import copyfileobj
|
||||
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from dateutil.parser import parse
|
||||
from slugify import slugify
|
||||
|
||||
from blinkpy import api
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.helpers.util import (
|
||||
http_req, create_session, BlinkURLHandler,
|
||||
BlinkException, BlinkAuthenticationException)
|
||||
create_session,
|
||||
merge_dicts,
|
||||
get_time,
|
||||
BlinkURLHandler,
|
||||
Throttle,
|
||||
)
|
||||
from blinkpy.helpers.constants import (
|
||||
DEFAULT_URL, BLINK_URL, LOGIN_URL, LOGIN_BACKUP_URL)
|
||||
|
||||
REFRESH_RATE = 30
|
||||
|
||||
_LOGGER = logging.getLogger('blinkpy')
|
||||
BLINK_URL,
|
||||
DEFAULT_MOTION_INTERVAL,
|
||||
DEFAULT_REFRESH,
|
||||
MIN_THROTTLE_TIME,
|
||||
LOGIN_URLS,
|
||||
)
|
||||
from blinkpy.helpers.constants import __version__
|
||||
from blinkpy.login_handler import LoginHandler
|
||||
|
||||
|
||||
class Blink():
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Blink:
|
||||
"""Class to initialize communication."""
|
||||
|
||||
def __init__(self, username=None, password=None,
|
||||
refresh_rate=REFRESH_RATE):
|
||||
def __init__(
|
||||
self,
|
||||
username=None,
|
||||
password=None,
|
||||
cred_file=None,
|
||||
refresh_rate=DEFAULT_REFRESH,
|
||||
motion_interval=DEFAULT_MOTION_INTERVAL,
|
||||
legacy_subdomain=False,
|
||||
no_prompt=False,
|
||||
persist_key=None,
|
||||
):
|
||||
"""
|
||||
Initialize Blink system.
|
||||
|
||||
:param username: Blink username (usually email address)
|
||||
: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.
|
||||
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).
|
||||
:param no_prompt: Set to TRUE if using an implementation that needs to
|
||||
suppress command-line output.
|
||||
:param persist_key: Location of persistant identifier.
|
||||
"""
|
||||
self._username = username
|
||||
self._password = password
|
||||
self.login_handler = LoginHandler(
|
||||
username=username,
|
||||
password=password,
|
||||
cred_file=cred_file,
|
||||
persist_key=persist_key,
|
||||
)
|
||||
self._token = None
|
||||
self._auth_header = None
|
||||
self._host = None
|
||||
self._events = []
|
||||
self._last_summary = None
|
||||
self._last_events = None
|
||||
self.network_id = None
|
||||
self.account_id = None
|
||||
self.client_id = None
|
||||
self.network_ids = []
|
||||
self.urls = None
|
||||
self.sync = None
|
||||
self.sync = CaseInsensitiveDict({})
|
||||
self.region = None
|
||||
self.region_id = None
|
||||
self.last_refresh = None
|
||||
self.refresh_rate = refresh_rate
|
||||
self.session = None
|
||||
self._login_url = LOGIN_URL
|
||||
self.session = create_session()
|
||||
self.networks = []
|
||||
self.cameras = CaseInsensitiveDict({})
|
||||
self.video_list = CaseInsensitiveDict({})
|
||||
self.login_url = LOGIN_URLS[0]
|
||||
self.login_urls = []
|
||||
self.motion_interval = motion_interval
|
||||
self.version = __version__
|
||||
self.legacy = legacy_subdomain
|
||||
self.no_prompt = no_prompt
|
||||
self.available = False
|
||||
self.key_required = False
|
||||
self.login_response = {}
|
||||
|
||||
@property
|
||||
def events(self):
|
||||
"""Get all events on server."""
|
||||
return self._events
|
||||
def auth_header(self):
|
||||
"""Return the authentication header."""
|
||||
return self._auth_header
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
@@ -73,106 +125,102 @@ class Blink():
|
||||
Method logs in and sets auth token, urls, and ids for future requests.
|
||||
Essentially this is just a wrapper function for ease of use.
|
||||
"""
|
||||
if self._username is None or self._password is None:
|
||||
self.login()
|
||||
else:
|
||||
if not self.available:
|
||||
self.get_auth_token()
|
||||
|
||||
self.get_ids()
|
||||
self.sync = BlinkSyncModule(self, self._auth_header)
|
||||
self.sync.get_videos()
|
||||
if self.sync.video_count > 0:
|
||||
self.sync.get_cameras()
|
||||
self.sync.set_links()
|
||||
self._events = self.events_request()
|
||||
if self.key_required and not self.no_prompt:
|
||||
email = self.login_handler.data["username"]
|
||||
key = input("Enter code sent to {}: ".format(email))
|
||||
result = self.login_handler.send_auth_key(self, key)
|
||||
self.key_required = not result
|
||||
self.setup_post_verify()
|
||||
elif not self.key_required:
|
||||
self.setup_post_verify()
|
||||
|
||||
def setup_post_verify(self):
|
||||
"""Initialize blink system after verification."""
|
||||
camera_list = self.get_cameras()
|
||||
networks = self.get_ids()
|
||||
for network_name, network_id in networks.items():
|
||||
if network_id not in camera_list.keys():
|
||||
camera_list[network_id] = {}
|
||||
_LOGGER.warning("No cameras found for %s", network_name)
|
||||
sync_module = BlinkSyncModule(
|
||||
self, network_name, network_id, camera_list[network_id]
|
||||
)
|
||||
sync_module.start()
|
||||
self.sync[network_name] = sync_module
|
||||
self.cameras = self.merge_cameras()
|
||||
|
||||
def login(self):
|
||||
"""Prompt user for username and password."""
|
||||
self._username = input("Username:")
|
||||
self._password = getpass.getpass("Password:")
|
||||
if self.get_auth_token():
|
||||
_LOGGER.info("Login successful!")
|
||||
return True
|
||||
_LOGGER.warning("Unable to login with %s.", self._username)
|
||||
return False
|
||||
"""Perform server login. DEPRECATED."""
|
||||
_LOGGER.warning(
|
||||
"Method is deprecated and will be removed in a future version. Please use the LoginHandler.login() method instead."
|
||||
)
|
||||
return self.login_handler.login(self)
|
||||
|
||||
def get_auth_token(self):
|
||||
def get_auth_token(self, is_retry=False):
|
||||
"""Retrieve the authentication token from Blink."""
|
||||
if not isinstance(self._username, str):
|
||||
raise BlinkAuthenticationException(ERROR.USERNAME)
|
||||
if not isinstance(self._password, str):
|
||||
raise BlinkAuthenticationException(ERROR.PASSWORD)
|
||||
|
||||
headers = {'Host': DEFAULT_URL,
|
||||
'Content-Type': 'application/json'}
|
||||
data = json.dumps({
|
||||
"email": self._username,
|
||||
"password": self._password,
|
||||
"client_specifier": "iPhone 9.2 | 2.2 | 222"
|
||||
})
|
||||
self.session = create_session()
|
||||
response = http_req(self, url=self._login_url, headers=headers,
|
||||
data=data, json_resp=False, reqtype='post')
|
||||
if response.status_code == 200:
|
||||
response = response.json()
|
||||
(self.region_id, self.region), = response['region'].items()
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
("Received response code %s "
|
||||
"when authenticating, "
|
||||
"trying new url"), response.status_code
|
||||
)
|
||||
self._login_url = LOGIN_BACKUP_URL
|
||||
response = http_req(self, url=self._login_url, headers=headers,
|
||||
data=data, reqtype='post')
|
||||
self.region_id = 'piri'
|
||||
self.region = "UNKNOWN"
|
||||
|
||||
self._host = "{}.{}".format(self.region_id, BLINK_URL)
|
||||
self._token = response['authtoken']['authtoken']
|
||||
|
||||
self._auth_header = {'Host': self._host,
|
||||
'TOKEN_AUTH': self._token}
|
||||
|
||||
self.urls = BlinkURLHandler(self.region_id)
|
||||
|
||||
self.login_response = self.login_handler.login(self)
|
||||
if not self.login_response:
|
||||
return False
|
||||
self.setup_params(self.login_response)
|
||||
if self.login_handler.check_key_required(self):
|
||||
self.key_required = True
|
||||
return self._auth_header
|
||||
|
||||
def setup_params(self, response):
|
||||
"""Retrieve blink parameters from login response."""
|
||||
self.login_url = self.login_handler.login_url
|
||||
((self.region_id, self.region),) = response["region"].items()
|
||||
self._host = "{}.{}".format(self.region_id, BLINK_URL)
|
||||
self._token = response["authtoken"]["authtoken"]
|
||||
self._auth_header = {"Host": self._host, "TOKEN_AUTH": self._token}
|
||||
self.urls = BlinkURLHandler(self.region_id, legacy=self.legacy)
|
||||
self.networks = self.get_networks()
|
||||
self.client_id = response["client"]["id"]
|
||||
self.account_id = response["account"]["id"]
|
||||
|
||||
def get_networks(self):
|
||||
"""Get network information."""
|
||||
response = api.request_networks(self)
|
||||
try:
|
||||
return response["summary"]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def get_ids(self):
|
||||
"""Set the network ID and Account ID."""
|
||||
response = self._network_request()
|
||||
self.network_id = str(response['networks'][0]['id'])
|
||||
self.account_id = str(response['networks'][0]['account_id'])
|
||||
all_networks = []
|
||||
network_dict = {}
|
||||
for network, status in self.networks.items():
|
||||
if status["onboarded"]:
|
||||
all_networks.append("{}".format(network))
|
||||
network_dict[status["name"]] = network
|
||||
|
||||
def _network_request(self):
|
||||
"""Get network and account information."""
|
||||
url = self.urls.networks_url
|
||||
headers = self._auth_header
|
||||
if headers is None:
|
||||
raise BlinkException(ERROR.AUTH_TOKEN)
|
||||
return http_req(self, url=url, headers=headers, reqtype='get')
|
||||
self.network_ids = all_networks
|
||||
return network_dict
|
||||
|
||||
def events_request(self, skip_throttle=False):
|
||||
"""Get events on server."""
|
||||
url = "{}/{}".format(self.urls.event_url, self.network_id)
|
||||
headers = self._auth_header
|
||||
if self.check_if_ok_to_update() or skip_throttle:
|
||||
self._last_events = http_req(self, url=url,
|
||||
headers=headers,
|
||||
reqtype='get')
|
||||
return self._last_events
|
||||
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] = []
|
||||
|
||||
def summary_request(self, skip_throttle=False):
|
||||
"""Get blink summary."""
|
||||
url = self.urls.home_url
|
||||
headers = self._auth_header
|
||||
if headers is None:
|
||||
raise BlinkException(ERROR.AUTH_TOKEN)
|
||||
if self.check_if_ok_to_update() or skip_throttle:
|
||||
self._last_summary = http_req(
|
||||
self, url=url, headers=headers, reqtype='get')
|
||||
return self._last_summary
|
||||
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):
|
||||
"""
|
||||
Perform a system refresh.
|
||||
@@ -180,10 +228,14 @@ class Blink():
|
||||
:param force_cache: Force an update of the camera cache
|
||||
"""
|
||||
if self.check_if_ok_to_update() or force_cache:
|
||||
_LOGGER.debug("Attempting refresh of cameras.")
|
||||
self._last_events = self.events_request(skip_throttle=True)
|
||||
self._last_summary = self.summary_request(skip_throttle=True)
|
||||
self.sync.refresh(force_cache=force_cache)
|
||||
for sync_name, sync_module in self.sync.items():
|
||||
_LOGGER.debug("Attempting refresh of sync %s", sync_name)
|
||||
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):
|
||||
"""Check if it is ok to perform an http request."""
|
||||
@@ -192,6 +244,93 @@ class Blink():
|
||||
if last_refresh is None:
|
||||
last_refresh = 0
|
||||
if current_time >= (last_refresh + self.refresh_rate):
|
||||
self.last_refresh = current_time
|
||||
return True
|
||||
return False
|
||||
|
||||
def merge_cameras(self):
|
||||
"""Merge all sync camera dicts into one."""
|
||||
combined = CaseInsensitiveDict({})
|
||||
for sync in self.sync:
|
||||
combined = merge_dicts(combined, self.sync[sync].cameras)
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
+107
-136
@@ -2,40 +2,31 @@
|
||||
|
||||
from shutil import copyfileobj
|
||||
import logging
|
||||
from requests.exceptions import RequestException
|
||||
from blinkpy import api
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MAX_CLIPS = 5
|
||||
|
||||
|
||||
class BlinkCamera():
|
||||
class BlinkCamera:
|
||||
"""Class to initialize individual camera."""
|
||||
|
||||
def __init__(self, config, sync):
|
||||
def __init__(self, sync):
|
||||
"""Initiailize BlinkCamera."""
|
||||
self.sync = sync
|
||||
self.urls = self.sync.urls
|
||||
self.id = str(config['device_id']) # pylint: disable=invalid-name
|
||||
self.name = config['name']
|
||||
self._status = config['active']
|
||||
self.thumbnail = "{}{}.jpg".format(self.urls.base_url,
|
||||
config['thumbnail'])
|
||||
self.clip = "{}{}".format(self.urls.base_url, config['video'])
|
||||
self.temperature = config['temp']
|
||||
self._battery_string = config['battery']
|
||||
self.notifications = config['notifications']
|
||||
self.motion = dict()
|
||||
self.header = None
|
||||
self.image_link = None
|
||||
self.arm_link = None
|
||||
self.region_id = config['region_id']
|
||||
self.battery_voltage = -180
|
||||
self.name = None
|
||||
self.camera_id = None
|
||||
self.network_id = None
|
||||
self.thumbnail = None
|
||||
self.serial = None
|
||||
self.motion_enabled = None
|
||||
self.battery_voltage = None
|
||||
self.clip = None
|
||||
self.temperature = None
|
||||
self.temperature_calibrated = None
|
||||
self.battery_state = None
|
||||
self.motion_detected = None
|
||||
self.wifi_strength = None
|
||||
self.camera_config = dict()
|
||||
self.motion_enabled = None
|
||||
self.last_record = list()
|
||||
self.last_record = None
|
||||
self._cached_image = None
|
||||
self._cached_video = None
|
||||
|
||||
@@ -43,48 +34,29 @@ class BlinkCamera():
|
||||
def attributes(self):
|
||||
"""Return dictionary of all camera attributes."""
|
||||
attributes = {
|
||||
'name': self.name,
|
||||
'device_id': self.id,
|
||||
'status': self._status,
|
||||
'armed': self.armed,
|
||||
'temperature': self.temperature,
|
||||
'temperature_c': self.temperature_c,
|
||||
'battery': self.battery,
|
||||
'thumbnail': self.thumbnail,
|
||||
'video': self.clip,
|
||||
'motion_enabled': self.motion_enabled,
|
||||
'notifications': self.notifications,
|
||||
'motion_detected': self.motion_detected,
|
||||
'wifi_strength': self.wifi_strength,
|
||||
'network_id': self.sync.network_id,
|
||||
'last_record': self.last_record
|
||||
"name": self.name,
|
||||
"camera_id": self.camera_id,
|
||||
"serial": self.serial,
|
||||
"temperature": self.temperature,
|
||||
"temperature_c": self.temperature_c,
|
||||
"temperature_calibrated": self.temperature_calibrated,
|
||||
"battery": self.battery,
|
||||
"battery_voltage": self.battery_voltage,
|
||||
"thumbnail": self.thumbnail,
|
||||
"video": self.clip,
|
||||
"motion_enabled": self.motion_enabled,
|
||||
"motion_detected": self.motion_detected,
|
||||
"wifi_strength": self.wifi_strength,
|
||||
"network_id": self.sync.network_id,
|
||||
"sync_module": self.sync.name,
|
||||
"last_record": self.last_record,
|
||||
}
|
||||
return attributes
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Return camera status."""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def armed(self):
|
||||
"""Return camera arm status."""
|
||||
return True if self._status == 'armed' else False
|
||||
|
||||
@property
|
||||
def battery(self):
|
||||
"""Return battery level as percentage."""
|
||||
return round(self.battery_voltage / 180 * 100)
|
||||
|
||||
@property
|
||||
def battery_string(self):
|
||||
"""Return string indicating battery status."""
|
||||
status = "Unknown"
|
||||
if self._battery_string > 1 and self._battery_string <= 3:
|
||||
status = "OK"
|
||||
elif self._battery_string >= 0:
|
||||
status = "Low"
|
||||
return status
|
||||
"""Return battery as string."""
|
||||
return self.battery_state
|
||||
|
||||
@property
|
||||
def temperature_c(self):
|
||||
@@ -107,90 +79,85 @@ class BlinkCamera():
|
||||
|
||||
def snap_picture(self):
|
||||
"""Take a picture with camera to create a new thumbnail."""
|
||||
self.sync.http_post(self.image_link)
|
||||
return api.request_new_image(self.sync.blink, self.network_id, self.camera_id)
|
||||
|
||||
def set_motion_detect(self, enable):
|
||||
"""Set motion detection."""
|
||||
url = self.arm_link
|
||||
if enable:
|
||||
self.sync.http_post("{}{}".format(url, 'enable'))
|
||||
return api.request_motion_detection_enable(
|
||||
self.sync.blink, self.network_id, self.camera_id
|
||||
)
|
||||
return api.request_motion_detection_disable(
|
||||
self.sync.blink, self.network_id, self.camera_id
|
||||
)
|
||||
|
||||
def update(self, config, force_cache=False, **kwargs):
|
||||
"""Update camera info."""
|
||||
# force = kwargs.pop('force', False)
|
||||
self.name = config["name"]
|
||||
self.camera_id = str(config["id"])
|
||||
self.network_id = str(config["network_id"])
|
||||
self.serial = config["serial"]
|
||||
self.motion_enabled = config["enabled"]
|
||||
self.battery_voltage = config["battery_voltage"]
|
||||
self.battery_state = config["battery_state"]
|
||||
self.temperature = config["temperature"]
|
||||
self.wifi_strength = config["wifi_strength"]
|
||||
|
||||
# Retrieve calibrated temperature from special endpoint
|
||||
resp = api.request_camera_sensors(
|
||||
self.sync.blink, self.network_id, self.camera_id
|
||||
)
|
||||
try:
|
||||
self.temperature_calibrated = resp["temp"]
|
||||
except KeyError:
|
||||
self.temperature_calibrated = self.temperature
|
||||
_LOGGER.warning("Could not retrieve calibrated temperature.")
|
||||
|
||||
# Check if thumbnail exists in config, if not try to
|
||||
# get it from the homescreen info in the sync module
|
||||
# otherwise set it to None and log an error
|
||||
new_thumbnail = None
|
||||
thumb_addr = None
|
||||
if config["thumbnail"]:
|
||||
thumb_addr = config["thumbnail"]
|
||||
else:
|
||||
self.sync.http_post("{}{}".format(url, 'disable'))
|
||||
_LOGGER.warning(
|
||||
"Could not find thumbnail for camera %s", self.name, exc_info=True
|
||||
)
|
||||
|
||||
def update(self, values, force_cache=False, skip_cache=False):
|
||||
"""Update camera information."""
|
||||
self.name = values['name']
|
||||
self._status = values['active']
|
||||
self.clip = "{}{}".format(
|
||||
self.urls.base_url, values['video'])
|
||||
new_thumbnail = "{}{}.jpg".format(
|
||||
self.urls.base_url, values['thumbnail'])
|
||||
self._battery_string = values['battery']
|
||||
self.notifications = values['notifications']
|
||||
if thumb_addr is not None:
|
||||
new_thumbnail = "{}{}.jpg".format(self.sync.urls.base_url, thumb_addr)
|
||||
|
||||
try:
|
||||
self.motion_detected = self.sync.motion[self.name]
|
||||
except KeyError:
|
||||
self.motion_detected = False
|
||||
|
||||
clip_addr = None
|
||||
if self.name in self.sync.last_record:
|
||||
clip_addr = self.sync.last_record[self.name]["clip"]
|
||||
self.last_record = self.sync.last_record[self.name]["time"]
|
||||
self.clip = "{}{}".format(self.sync.urls.base_url, clip_addr)
|
||||
|
||||
# If the thumbnail or clip have changed, update the cache
|
||||
update_cached_image = False
|
||||
if new_thumbnail != self.thumbnail or self._cached_image is None:
|
||||
update_cached_image = True
|
||||
self.thumbnail = new_thumbnail
|
||||
try:
|
||||
cfg = self.sync.camera_config_request(self.id)
|
||||
self.camera_config = cfg
|
||||
except RequestException as err:
|
||||
_LOGGER.warning("Could not get config for %s with id %s",
|
||||
self.name, self.id)
|
||||
_LOGGER.warning("Exception raised: %s", err)
|
||||
|
||||
try:
|
||||
self.battery_voltage = cfg['camera'][0]['battery_voltage']
|
||||
self.motion_enabled = cfg['camera'][0]['motion_alert']
|
||||
self.wifi_strength = cfg['camera'][0]['wifi_strength']
|
||||
self.temperature = cfg['camera'][0]['temperature']
|
||||
except KeyError:
|
||||
_LOGGER.warning("Problem extracting config for camera %s",
|
||||
self.name)
|
||||
update_cached_video = False
|
||||
if self._cached_video is None or self.motion_detected:
|
||||
update_cached_video = True
|
||||
|
||||
# Check if the most recent clip is included in the last_record list
|
||||
# and that the last_record list is populated
|
||||
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.warning("Could not extract clip info from camera %s",
|
||||
self.name)
|
||||
|
||||
if not skip_cache:
|
||||
if update_cached_image or force_cache:
|
||||
self._cached_image = self.sync.http_get(
|
||||
self.image_refresh(), stream=True, json=False)
|
||||
if (self.clip is None) or self.motion_detected or force_cache:
|
||||
self._cached_video = self.sync.http_get(
|
||||
self.clip, stream=True, json=False)
|
||||
|
||||
def image_refresh(self):
|
||||
"""Refresh current thumbnail."""
|
||||
url = self.urls.home_url
|
||||
response = self.sync.http_get(url)['devices']
|
||||
for element in response:
|
||||
try:
|
||||
if str(element['device_id']) == self.id:
|
||||
self.thumbnail = (
|
||||
"{}{}.jpg".format(
|
||||
self.urls.base_url, element['thumbnail'])
|
||||
)
|
||||
return self.thumbnail
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
if new_thumbnail is not None and (update_cached_image or force_cache):
|
||||
self._cached_image = api.http_get(
|
||||
self.sync.blink, url=self.thumbnail, stream=True, json=False
|
||||
)
|
||||
if clip_addr is not None and (update_cached_video or force_cache):
|
||||
self._cached_video = api.http_get(
|
||||
self.sync.blink, url=self.clip, stream=True, json=False
|
||||
)
|
||||
|
||||
def image_to_file(self, path):
|
||||
"""
|
||||
@@ -201,11 +168,12 @@ class BlinkCamera():
|
||||
_LOGGER.debug("Writing image from %s to %s", self.name, path)
|
||||
response = self._cached_image
|
||||
if response.status_code == 200:
|
||||
with open(path, 'wb') as imgfile:
|
||||
with open(path, "wb") as imgfile:
|
||||
copyfileobj(response.raw, imgfile)
|
||||
else:
|
||||
_LOGGER.error("Cannot write image to file, response %s",
|
||||
response.status_code)
|
||||
_LOGGER.error(
|
||||
"Cannot write image to file, response %s", response.status_code
|
||||
)
|
||||
|
||||
def video_to_file(self, path):
|
||||
"""Write video to file.
|
||||
@@ -214,5 +182,8 @@ class BlinkCamera():
|
||||
"""
|
||||
_LOGGER.debug("Writing video from %s to %s", self.name, path)
|
||||
response = self._cached_video
|
||||
with open(path, 'wb') as vidfile:
|
||||
if response is None:
|
||||
_LOGGER.error("No saved video exist for %s.", self.name)
|
||||
return
|
||||
with open(path, "wb") as vidfile:
|
||||
copyfileobj(response.raw, vidfile)
|
||||
|
||||
@@ -3,52 +3,68 @@
|
||||
import os
|
||||
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 9
|
||||
PATCH_VERSION = 0
|
||||
MINOR_VERSION = 15
|
||||
PATCH_VERSION = "0-rc.0"
|
||||
|
||||
__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
|
||||
__version__ = "{}.{}.{}".format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
|
||||
|
||||
REQUIRED_PYTHON_VER = (3, 4, 2)
|
||||
REQUIRED_PYTHON_VER = (3, 5, 3)
|
||||
|
||||
PROJECT_NAME = 'blinkpy'
|
||||
PROJECT_PACKAGE_NAME = 'blinkpy'
|
||||
PROJECT_LICENSE = 'MIT'
|
||||
PROJECT_AUTHOR = 'Kevin Fronczak'
|
||||
PROJECT_COPYRIGHT = ' 2017, {}'.format(PROJECT_AUTHOR)
|
||||
PROJECT_URL = 'https://github.com/fronzbot/blinkpy'
|
||||
PROJECT_EMAIL = 'kfronczak@gmail.com'
|
||||
PROJECT_DESCRIPTION = ('A Blink camera Python library '
|
||||
'running on Python 3.')
|
||||
PROJECT_LONG_DESCRIPTION = ('blinkpy is an open-source '
|
||||
'unofficial API for the Blink Camera '
|
||||
'system with the intention for easy '
|
||||
'integration into various home '
|
||||
'automation platforms.')
|
||||
if os.path.exists('README.rst'):
|
||||
PROJECT_LONG_DESCRIPTION = open('README.rst').read()
|
||||
PROJECT_NAME = "blinkpy"
|
||||
PROJECT_PACKAGE_NAME = "blinkpy"
|
||||
PROJECT_LICENSE = "MIT"
|
||||
PROJECT_AUTHOR = "Kevin Fronczak"
|
||||
PROJECT_COPYRIGHT = " 2017, {}".format(PROJECT_AUTHOR)
|
||||
PROJECT_URL = "https://github.com/fronzbot/blinkpy"
|
||||
PROJECT_EMAIL = "kfronczak@gmail.com"
|
||||
PROJECT_DESCRIPTION = "A Blink camera Python library " "running on Python 3."
|
||||
PROJECT_LONG_DESCRIPTION = (
|
||||
"blinkpy is an open-source "
|
||||
"unofficial API for the Blink Camera "
|
||||
"system with the intention for easy "
|
||||
"integration into various home "
|
||||
"automation platforms."
|
||||
)
|
||||
if os.path.exists("README.rst"):
|
||||
PROJECT_LONG_DESCRIPTION = open("README.rst").read()
|
||||
PROJECT_CLASSIFIERS = [
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Topic :: Home Automation'
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Topic :: Home Automation",
|
||||
]
|
||||
|
||||
PROJECT_GITHUB_USERNAME = 'fronzbot'
|
||||
PROJECT_GITHUB_REPOSITORY = 'blinkpy'
|
||||
PROJECT_GITHUB_USERNAME = "fronzbot"
|
||||
PROJECT_GITHUB_REPOSITORY = "blinkpy"
|
||||
|
||||
PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME)
|
||||
PYPI_URL = "https://pypi.python.org/pypi/{}".format(PROJECT_PACKAGE_NAME)
|
||||
|
||||
'''
|
||||
"""
|
||||
URLS
|
||||
'''
|
||||
BLINK_URL = 'immedia-semi.com'
|
||||
LOGIN_URL = 'https://prod.' + BLINK_URL + '/login'
|
||||
LOGIN_BACKUP_URL = 'https://rest.piri/' + BLINK_URL + '/login'
|
||||
BASE_URL = 'https://prod.' + BLINK_URL
|
||||
DEFAULT_URL = 'prod.' + BLINK_URL
|
||||
"""
|
||||
BLINK_URL = "immedia-semi.com"
|
||||
DEFAULT_URL = "{}.{}".format("rest-prod", BLINK_URL)
|
||||
BASE_URL = "https://{}".format(DEFAULT_URL)
|
||||
LOGIN_URLS = [
|
||||
"{}/api/v4/login".format(BASE_URL),
|
||||
"{}/api/v3/login".format(BASE_URL),
|
||||
"{}/api/v2/login".format(BASE_URL),
|
||||
]
|
||||
|
||||
'''
|
||||
"""
|
||||
Dictionaries
|
||||
'''
|
||||
ONLINE = {'online': True, 'offline': False}
|
||||
"""
|
||||
ONLINE = {"online": True, "offline": False}
|
||||
|
||||
"""
|
||||
OTHER
|
||||
"""
|
||||
TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
|
||||
DEFAULT_MOTION_INTERVAL = 1
|
||||
DEFAULT_REFRESH = 30
|
||||
MIN_THROTTLE_TIME = 2
|
||||
SIZE_NOTIFICATION_KEY = 152
|
||||
SIZE_UID = 16
|
||||
|
||||
@@ -4,10 +4,12 @@ USERNAME = (0, "Username must be a string")
|
||||
PASSWORD = (1, "Password must be a string")
|
||||
AUTHENTICATE = (
|
||||
2,
|
||||
"Cannot authenticate since either password or username has not been set"
|
||||
"Cannot authenticate since either password or username has not been set",
|
||||
)
|
||||
AUTH_TOKEN = (
|
||||
3,
|
||||
"Authentication header incorrect. Are you sure you received your token?"
|
||||
"Authentication header incorrect. Are you sure you received your token?",
|
||||
)
|
||||
REQUEST = (4, "Cannot perform request (get/post type incorrect)")
|
||||
|
||||
BLINK_ERRORS = [101, 400, 404]
|
||||
|
||||
+142
-26
@@ -1,30 +1,82 @@
|
||||
"""Useful functions for blinkpy."""
|
||||
|
||||
import logging
|
||||
from requests import Request, Session
|
||||
import time
|
||||
import secrets
|
||||
from calendar import timegm
|
||||
from functools import partial, wraps
|
||||
from requests import Request, Session, exceptions
|
||||
import dateutil.parser
|
||||
from blinkpy.helpers.constants import BLINK_URL, TIMESTAMP_FORMAT
|
||||
import blinkpy.helpers.errors as ERROR
|
||||
from blinkpy.helpers.constants import BLINK_URL
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def gen_uid(size):
|
||||
"""Create a random sring."""
|
||||
full_token = secrets.token_hex(size)
|
||||
return full_token[0:size]
|
||||
|
||||
|
||||
def time_to_seconds(timestamp):
|
||||
"""Convert TIMESTAMP_FORMAT time to seconds."""
|
||||
try:
|
||||
dtime = dateutil.parser.isoparse(timestamp)
|
||||
except ValueError:
|
||||
_LOGGER.error("Incorrect timestamp format for conversion: %s.", timestamp)
|
||||
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):
|
||||
"""Merge two dictionaries into one."""
|
||||
duplicates = [val for val in dict_a if val in dict_b]
|
||||
if duplicates:
|
||||
_LOGGER.warning(
|
||||
("Duplicates found during merge: %s. " "Renaming is recommended."),
|
||||
duplicates,
|
||||
)
|
||||
return {**dict_a, **dict_b}
|
||||
|
||||
|
||||
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.get = partial(sess.get, timeout=5)
|
||||
return sess
|
||||
|
||||
|
||||
def attempt_reauthorization(blink):
|
||||
"""Attempt to refresh auth token and links."""
|
||||
_LOGGER.debug("Auth token expired, attempting reauthorization.")
|
||||
headers = blink.get_auth_token()
|
||||
blink.sync.set_links()
|
||||
_LOGGER.info("Auth token expired, attempting reauthorization.")
|
||||
headers = blink.get_auth_token(is_retry=True)
|
||||
return headers
|
||||
|
||||
|
||||
def http_req(blink, url='http://example.com', data=None, headers=None,
|
||||
reqtype='get', stream=False, json_resp=True, is_retry=False):
|
||||
def http_req(
|
||||
blink,
|
||||
url="http://example.com",
|
||||
data=None,
|
||||
headers=None,
|
||||
reqtype="get",
|
||||
stream=False,
|
||||
json_resp=True,
|
||||
is_retry=False,
|
||||
):
|
||||
"""
|
||||
Perform server requests and check if reauthorization neccessary.
|
||||
|
||||
@@ -37,25 +89,57 @@ def http_req(blink, url='http://example.com', data=None, headers=None,
|
||||
:param json_resp: Return JSON response? TRUE/False
|
||||
:param is_retry: Is this a retry attempt? True/FALSE
|
||||
"""
|
||||
if reqtype == 'post':
|
||||
req = Request('POST', url, headers=headers, data=data)
|
||||
elif reqtype == 'get':
|
||||
req = Request('GET', url, headers=headers)
|
||||
if reqtype == "post":
|
||||
req = Request("POST", url, headers=headers, data=data)
|
||||
elif reqtype == "get":
|
||||
req = Request("GET", url, headers=headers)
|
||||
else:
|
||||
_LOGGER.error("Invalid request type: %s", reqtype)
|
||||
raise BlinkException(ERROR.REQUEST)
|
||||
|
||||
prepped = req.prepare()
|
||||
response = blink.session.send(prepped, stream=stream)
|
||||
|
||||
if json_resp and 'code' in response.json():
|
||||
if is_retry:
|
||||
raise BlinkAuthenticationException(
|
||||
(response.json()['code'], response.json()['message']))
|
||||
else:
|
||||
try:
|
||||
response = blink.session.send(prepped, stream=stream)
|
||||
if json_resp and "code" in response.json():
|
||||
resp_dict = response.json()
|
||||
code = resp_dict["code"]
|
||||
message = resp_dict["message"]
|
||||
if is_retry and code in ERROR.BLINK_ERRORS:
|
||||
_LOGGER.error("Cannot obtain new token for server auth.")
|
||||
return None
|
||||
elif code in ERROR.BLINK_ERRORS:
|
||||
headers = attempt_reauthorization(blink)
|
||||
if not headers:
|
||||
raise exceptions.ConnectionError
|
||||
return http_req(
|
||||
blink,
|
||||
url=url,
|
||||
data=data,
|
||||
headers=headers,
|
||||
reqtype=reqtype,
|
||||
stream=stream,
|
||||
json_resp=json_resp,
|
||||
is_retry=True,
|
||||
)
|
||||
_LOGGER.warning("Response from server: %s - %s", code, message)
|
||||
|
||||
except (exceptions.ConnectionError, exceptions.Timeout):
|
||||
_LOGGER.info("Cannot connect to server with url %s.", url)
|
||||
if not is_retry:
|
||||
headers = attempt_reauthorization(blink)
|
||||
return http_req(blink, url=url, data=data, headers=headers,
|
||||
reqtype=reqtype, stream=stream,
|
||||
json_resp=json_resp, is_retry=True)
|
||||
return http_req(
|
||||
blink,
|
||||
url=url,
|
||||
data=data,
|
||||
headers=headers,
|
||||
reqtype=reqtype,
|
||||
stream=stream,
|
||||
json_resp=json_resp,
|
||||
is_retry=True,
|
||||
)
|
||||
_LOGGER.error("Endpoint %s failed. Possible issue with Blink servers.", url)
|
||||
return None
|
||||
|
||||
if json_resp:
|
||||
return response.json()
|
||||
@@ -76,18 +160,50 @@ class BlinkException(Exception):
|
||||
class BlinkAuthenticationException(BlinkException):
|
||||
"""Class to throw authentication exception."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BlinkURLHandler():
|
||||
class BlinkURLHandler:
|
||||
"""Class that handles Blink URLS."""
|
||||
|
||||
def __init__(self, region_id):
|
||||
def __init__(self, region_id, legacy=False):
|
||||
"""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.event_url = "{}/events/network".format(self.base_url)
|
||||
self.network_url = "{}/network".format(self.base_url)
|
||||
self.networks_url = "{}/networks".format(self.base_url)
|
||||
self.video_url = "{}/api/v2/videos".format(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
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Login handler for blink."""
|
||||
import json
|
||||
import logging
|
||||
from os.path import isfile
|
||||
from getpass import getpass
|
||||
from blinkpy import api
|
||||
from blinkpy.helpers import util
|
||||
from blinkpy.helpers import constants as const
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoginHandler:
|
||||
"""Class to handle login communication."""
|
||||
|
||||
def __init__(self, username=None, password=None, cred_file=None, persist_key=None):
|
||||
"""
|
||||
Initialize login handler.
|
||||
|
||||
:param username: Blink username
|
||||
:param password: Blink password
|
||||
:param cred_file: JSON formatted credential file.
|
||||
:param persist_key: File location of persistant key.
|
||||
"""
|
||||
self.login_url = None
|
||||
self.login_urls = const.LOGIN_URLS
|
||||
self.cred_file = cred_file
|
||||
self.persist_key = persist_key
|
||||
self.data = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"uid": None,
|
||||
"notification_key": None,
|
||||
}
|
||||
|
||||
self.check_keys()
|
||||
|
||||
def check_keys(self):
|
||||
"""Check if uid exists, if not create."""
|
||||
uid = util.gen_uid(const.SIZE_UID)
|
||||
notification_key = util.gen_uid(const.SIZE_NOTIFICATION_KEY)
|
||||
data = {"uid": uid, "notification_key": notification_key}
|
||||
if self.persist_key is None:
|
||||
return data
|
||||
if not isfile(self.persist_key):
|
||||
with open(self.persist_key, "w") as json_file:
|
||||
json.dump(data, json_file)
|
||||
else:
|
||||
with open(self.persist_key, "r") as json_file:
|
||||
data = json.load(json_file)
|
||||
return data
|
||||
|
||||
def check_cred_file(self):
|
||||
"""Check if credential file supplied and use if so."""
|
||||
if isfile(self.cred_file):
|
||||
try:
|
||||
with open(self.cred_file, "r") as json_file:
|
||||
creds = json.load(json_file)
|
||||
self.data["username"] = creds["username"]
|
||||
self.data["password"] = creds["password"]
|
||||
|
||||
except ValueError:
|
||||
_LOGGER.error(
|
||||
"Improperly formatted json file %s.", self.cred_file, exc_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
except KeyError:
|
||||
_LOGGER.error("JSON file information incomplete %s.", exc_info=True)
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_login(self):
|
||||
"""Check login information and prompt if not available."""
|
||||
if self.data["username"] is None:
|
||||
self.data["username"] = input("Username:")
|
||||
if self.data["password"] is None:
|
||||
self.data["password"] = getpass("Password:")
|
||||
|
||||
if self.data["username"] and self.data["password"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def validate_response(self, url, response):
|
||||
"""Validate response from login endpoint."""
|
||||
try:
|
||||
if response.status_code != 200:
|
||||
return False
|
||||
except AttributeError:
|
||||
_LOGGER.error(
|
||||
"Response for %s did not return a status code. Deprecated endpoint?",
|
||||
url,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
def login(self, blink):
|
||||
"""Attempt login to blink servers."""
|
||||
if self.cred_file is not None:
|
||||
self.check_cred_file()
|
||||
if not self.check_login():
|
||||
_LOGGER.error("Cannot login with username %s", self.data["username"])
|
||||
return False
|
||||
|
||||
for url in self.login_urls:
|
||||
_LOGGER.info("Attempting login with %s", url)
|
||||
response = api.request_login(
|
||||
blink,
|
||||
url,
|
||||
self.data["username"],
|
||||
self.data["password"],
|
||||
self.data["notification_key"],
|
||||
self.data["uid"],
|
||||
is_retry=False,
|
||||
)
|
||||
|
||||
if self.validate_response(url, response):
|
||||
self.login_url = url
|
||||
return response.json()
|
||||
|
||||
_LOGGER.error("Failed to login to Blink servers. Last response: %s", response)
|
||||
return False
|
||||
|
||||
def send_auth_key(self, blink, key):
|
||||
"""Send 2FA key to blink servers."""
|
||||
if key is not None:
|
||||
response = api.request_verify(blink, key)
|
||||
try:
|
||||
json_resp = response.json()
|
||||
blink.available = json_resp["valid"]
|
||||
except (KeyError, TypeError):
|
||||
_LOGGER.error("Did not receive valid response from server.")
|
||||
return False
|
||||
return True
|
||||
|
||||
def check_key_required(self, blink):
|
||||
"""Check if 2FA key is required."""
|
||||
try:
|
||||
if blink.login_response["client"]["verification_required"]:
|
||||
return True
|
||||
except KeyError:
|
||||
pass
|
||||
return False
|
||||
+152
-202
@@ -3,249 +3,199 @@
|
||||
import logging
|
||||
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from blinkpy import api
|
||||
from blinkpy.camera import BlinkCamera
|
||||
from blinkpy.helpers.util import http_req
|
||||
from blinkpy.helpers.util import time_to_seconds
|
||||
from blinkpy.helpers.constants import ONLINE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BlinkSyncModule():
|
||||
class BlinkSyncModule:
|
||||
"""Class to initialize sync module."""
|
||||
|
||||
def __init__(self, blink, header, urls=None):
|
||||
def __init__(self, blink, network_name, network_id, camera_list):
|
||||
"""
|
||||
Initialize Blink sync module.
|
||||
|
||||
:param blink: Blink class instantiation
|
||||
:param header: Blink authentication header
|
||||
:param urls: URL Handler instantiation (deprecated)
|
||||
"""
|
||||
self.blink = blink
|
||||
self._auth_header = header
|
||||
self._auth_header = blink.auth_header
|
||||
self.network_id = network_id
|
||||
self.region = blink.region
|
||||
self.region_id = blink.region_id
|
||||
self.name = network_name
|
||||
self.serial = None
|
||||
self.status = None
|
||||
self.sync_id = None
|
||||
self.region = None
|
||||
self.region_id = None
|
||||
self._host = None
|
||||
self._events = []
|
||||
self.host = None
|
||||
self.summary = None
|
||||
self.network_info = None
|
||||
self.events = []
|
||||
self.cameras = CaseInsensitiveDict({})
|
||||
self._idlookup = {}
|
||||
self._video_count = 0
|
||||
self._all_videos = {}
|
||||
self._summary = None
|
||||
self.record_dates = dict()
|
||||
self.first_init = True
|
||||
self.motion_interval = blink.motion_interval
|
||||
self.motion = {}
|
||||
self.last_record = {}
|
||||
self.camera_list = camera_list
|
||||
self.available = False
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
"""Return sync attributes."""
|
||||
attr = {
|
||||
"name": self.name,
|
||||
"id": self.sync_id,
|
||||
"network_id": self.network_id,
|
||||
"serial": self.serial,
|
||||
"status": self.status,
|
||||
"region": self.region,
|
||||
"region_id": self.region_id,
|
||||
}
|
||||
return attr
|
||||
|
||||
@property
|
||||
def urls(self):
|
||||
"""Return device urls."""
|
||||
return self.blink.urls
|
||||
|
||||
@property
|
||||
def network_id(self):
|
||||
"""Return the network id."""
|
||||
return self.blink.network_id
|
||||
|
||||
@property
|
||||
def camera_thumbs(self):
|
||||
"""Return camera thumbnails."""
|
||||
self.blink.refresh()
|
||||
data = {}
|
||||
for name, camera in self.cameras.items():
|
||||
data[name] = camera.thumbnail
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def id_table(self):
|
||||
"""Return id/camera pairs."""
|
||||
return self._idlookup
|
||||
|
||||
@property
|
||||
def video_count(self):
|
||||
"""Return number of videos on server."""
|
||||
url = "{}/count".format(self.urls.video_url)
|
||||
self._video_count = self.http_get(url)['count']
|
||||
return self._video_count
|
||||
|
||||
@property
|
||||
def events(self):
|
||||
"""Get all events on server."""
|
||||
return self._events
|
||||
|
||||
@property
|
||||
def online(self):
|
||||
"""Return boolean system online status."""
|
||||
return ONLINE[self._status_request()['syncmodule']['status']]
|
||||
|
||||
@property
|
||||
def videos(self):
|
||||
"""Return video list."""
|
||||
return self._all_videos
|
||||
|
||||
@property
|
||||
def summary(self):
|
||||
"""Get a full summary of device information."""
|
||||
return self._summary
|
||||
return ONLINE[self.status]
|
||||
|
||||
@property
|
||||
def arm(self):
|
||||
"""Return status of sync module: armed/disarmed."""
|
||||
return self._summary['network']['armed']
|
||||
try:
|
||||
return self.network_info["network"]["armed"]
|
||||
except (KeyError, TypeError):
|
||||
return None
|
||||
|
||||
@arm.setter
|
||||
def arm(self, value):
|
||||
"""Arm or disarm system."""
|
||||
if value:
|
||||
value_to_append = 'arm'
|
||||
else:
|
||||
value_to_append = 'disarm'
|
||||
url = "{}/{}/{}".format(self.urls.network_url,
|
||||
self.network_id,
|
||||
value_to_append)
|
||||
self.http_post(url)
|
||||
return api.request_system_arm(self.blink, self.network_id)
|
||||
|
||||
return api.request_system_disarm(self.blink, self.network_id)
|
||||
|
||||
def start(self):
|
||||
"""Initialize the system."""
|
||||
response = api.request_syncmodule(self.blink, self.network_id)
|
||||
try:
|
||||
self.summary = response["syncmodule"]
|
||||
self.network_id = self.summary["network_id"]
|
||||
except (TypeError, KeyError):
|
||||
_LOGGER.error(
|
||||
("Could not retrieve sync module information " "with response: %s"),
|
||||
response,
|
||||
exc_info=True,
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
self.sync_id = self.summary["id"]
|
||||
self.serial = self.summary["serial"]
|
||||
self.status = self.summary["status"]
|
||||
except KeyError:
|
||||
_LOGGER.error(
|
||||
"Could not extract some sync module info: %s", response, exc_info=True
|
||||
)
|
||||
|
||||
self.get_network_info()
|
||||
self.check_new_videos()
|
||||
try:
|
||||
for camera_config in self.camera_list:
|
||||
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.available = True
|
||||
return True
|
||||
|
||||
def get_events(self, **kwargs):
|
||||
"""Retrieve events from server."""
|
||||
force = kwargs.pop("force", False)
|
||||
response = api.request_sync_events(self.blink, self.network_id, force=force)
|
||||
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, camera_id):
|
||||
"""Retrieve camera information."""
|
||||
response = api.request_camera_info(self.blink, self.network_id, camera_id)
|
||||
try:
|
||||
return response["camera"][0]
|
||||
except (TypeError, KeyError):
|
||||
_LOGGER.error("Could not extract camera info: %s", response, exc_info=True)
|
||||
return []
|
||||
|
||||
def get_network_info(self):
|
||||
"""Retrieve network status."""
|
||||
is_errored = False
|
||||
self.network_info = api.request_network_status(self.blink, self.network_id)
|
||||
try:
|
||||
is_errored = self.network_info["network"]["sync_module_error"]
|
||||
except KeyError:
|
||||
is_errored = True
|
||||
|
||||
if is_errored:
|
||||
self.available = False
|
||||
|
||||
def refresh(self, force_cache=False):
|
||||
"""Get all blink cameras and pulls their most recent status."""
|
||||
summary = self._summary_request()
|
||||
events = self._events_request()
|
||||
response = summary['devices']
|
||||
self.get_videos()
|
||||
for name in self.cameras:
|
||||
camera = self.cameras[name]
|
||||
for element in response:
|
||||
try:
|
||||
if str(element['device_id']) == camera.id:
|
||||
element['video'] = self.videos[name][0]['clip']
|
||||
thumb = self.videos[name][0]['thumb']
|
||||
element['thumbnail'] = thumb
|
||||
camera.update(element, force_cache=force_cache)
|
||||
except KeyError:
|
||||
pass
|
||||
self._summary = summary
|
||||
self._events = events
|
||||
self.get_network_info()
|
||||
self.check_new_videos()
|
||||
for camera_name in self.cameras.keys():
|
||||
camera_id = self.cameras[camera_name].camera_id
|
||||
camera_info = self.get_camera_info(camera_id)
|
||||
self.cameras[camera_name].update(camera_info, force_cache=force_cache)
|
||||
|
||||
def get_videos(self, start_page=0, end_page=1):
|
||||
"""
|
||||
Retrieve last recorded videos per camera.
|
||||
def check_new_videos(self):
|
||||
"""Check if new videos since last refresh."""
|
||||
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
|
||||
(defaults to 0)
|
||||
:param end_page: Page to stop reading from (defaults to 1)
|
||||
"""
|
||||
videos = list()
|
||||
all_dates = dict()
|
||||
for page_num in range(start_page, end_page + 1):
|
||||
this_page = self._video_request(page_num)
|
||||
if not this_page:
|
||||
break
|
||||
videos.append(this_page)
|
||||
resp = api.request_videos(self.blink, time=interval, page=1)
|
||||
|
||||
for page in videos:
|
||||
_LOGGER.debug("Retrieved video page %s", page)
|
||||
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]
|
||||
if camera_name not in all_dates:
|
||||
all_dates[camera_name] = list()
|
||||
all_dates[camera_name].append(clip_date)
|
||||
try:
|
||||
self._all_videos[camera_name].append(
|
||||
{
|
||||
'clip': clip_addr,
|
||||
'thumb': thumb_addr,
|
||||
}
|
||||
)
|
||||
except KeyError:
|
||||
self._all_videos[camera_name] = [
|
||||
{
|
||||
'clip': clip_addr,
|
||||
'thumb': thumb_addr,
|
||||
}
|
||||
]
|
||||
self.record_dates = all_dates
|
||||
for camera in self.cameras.keys():
|
||||
self.motion[camera] = False
|
||||
|
||||
def get_cameras(self):
|
||||
"""Find and creates cameras."""
|
||||
self._summary = self._summary_request()
|
||||
response = self._summary['devices']
|
||||
for element in response:
|
||||
if ('device_type' in element and
|
||||
element['device_type'] == 'camera'):
|
||||
# Add region to config
|
||||
element['region_id'] = self.region_id
|
||||
try:
|
||||
name = element['name']
|
||||
element['video'] = self.videos[name][0]['clip']
|
||||
element['thumbnail'] = self.videos[name][0]['thumb']
|
||||
except KeyError:
|
||||
element['video'] = None
|
||||
element['thumbnail'] = None
|
||||
device = BlinkCamera(element, self)
|
||||
self.cameras[device.name] = device
|
||||
self._idlookup[device.id] = device.name
|
||||
self.blink.refresh(force_cache=self.first_init)
|
||||
self.first_init = False
|
||||
try:
|
||||
info = resp["media"]
|
||||
except (KeyError, TypeError):
|
||||
_LOGGER.warning("Could not check for motion. Response: %s", resp)
|
||||
return False
|
||||
|
||||
def set_links(self):
|
||||
"""Set access links and required headers for each camera in system."""
|
||||
for name in self.cameras:
|
||||
camera = self.cameras[name]
|
||||
network_id_url = "{}/{}".format(self.urls.network_url,
|
||||
self.network_id)
|
||||
image_url = "{}/camera/{}/thumbnail".format(network_id_url,
|
||||
camera.id)
|
||||
arm_url = "{}/camera/{}/".format(network_id_url,
|
||||
camera.id)
|
||||
camera.image_link = image_url
|
||||
camera.arm_link = arm_url
|
||||
camera.header = self._auth_header
|
||||
for entry in info:
|
||||
try:
|
||||
name = entry["device_name"]
|
||||
clip = entry["media"]
|
||||
timestamp = entry["created_at"]
|
||||
if self.check_new_video_time(timestamp):
|
||||
self.motion[name] = True and self.arm
|
||||
self.last_record[name] = {"clip": clip, "time": timestamp}
|
||||
except KeyError:
|
||||
_LOGGER.debug("No new videos since last refresh.")
|
||||
|
||||
def _summary_request(self):
|
||||
"""Request a summary from blink."""
|
||||
return self.blink.summary_request()
|
||||
return True
|
||||
|
||||
def _events_request(self):
|
||||
"""Request a list of events from blink."""
|
||||
return self.blink.events_request()
|
||||
|
||||
def _video_request(self, page=0):
|
||||
"""Perform a request for videos."""
|
||||
url = "{}/page/{}".format(self.urls.video_url, page)
|
||||
return self.http_get(url)
|
||||
|
||||
def _status_request(self):
|
||||
"""Get syncmodule status."""
|
||||
url = "{}/{}/syncmodules".format(self.urls.network_url,
|
||||
self.network_id)
|
||||
return self.http_get(url)
|
||||
|
||||
def camera_config_request(self, camera_id):
|
||||
"""Retrieve more info about Blink config."""
|
||||
url = "{}/network/{}/camera/{}/config".format(self.urls.base_url,
|
||||
self.network_id,
|
||||
str(camera_id))
|
||||
return self.http_get(url)
|
||||
|
||||
def http_get(self, url, stream=False, json=True):
|
||||
"""
|
||||
Perform an http get request.
|
||||
|
||||
:param url: URL to perform get request
|
||||
:param stream: Stream response? True/FALSE
|
||||
:param json: Return json response? TRUE/False
|
||||
"""
|
||||
return http_req(self.blink, url=url, headers=self._auth_header,
|
||||
reqtype='get', stream=stream, json_resp=json)
|
||||
|
||||
def http_post(self, url):
|
||||
"""Perform a post request.
|
||||
|
||||
:param url: URL to perform post request
|
||||
"""
|
||||
return http_req(self.blink, url=url, headers=self._auth_header,
|
||||
reqtype='post')
|
||||
def check_new_video_time(self, timestamp):
|
||||
"""Check if video has timestamp since last refresh."""
|
||||
return time_to_seconds(timestamp) > self.blink.last_refresh
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
codecov:
|
||||
branch: dev
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: "2%"
|
||||
|
||||
comment: true
|
||||
require_ci_to_pass: yes
|
||||
range: 65..90
|
||||
round: down
|
||||
precision: 1
|
||||
+4
-17
@@ -1,30 +1,17 @@
|
||||
.. _core_module:
|
||||
|
||||
:mod:`blinkpy`
|
||||
Blinkpy
|
||||
----------------------
|
||||
|
||||
.. automodule:: blinkpy.blinkpy
|
||||
:members:
|
||||
|
||||
.. automodule:: blinkpy.sync_module
|
||||
:members:
|
||||
|
||||
.. automodule:: blinkpy.camera
|
||||
:members:
|
||||
|
||||
.. automodule:: blinkpy.helpers.util
|
||||
|
||||
.. autoclass:: blinkpy.blinkpy.Blink
|
||||
:members:
|
||||
|
||||
.. autoclass:: blinkpy.sync_module.BlinkSyncModule
|
||||
:members:
|
||||
|
||||
.. autoclass:: blinkpy.camera.BlinkCamera
|
||||
:members:
|
||||
|
||||
.. autoclass:: blinkpy.helpers.util.BlinkURLHandler
|
||||
:members:
|
||||
|
||||
.. autofunction:: blinkpy.helpers.util.create_session
|
||||
|
||||
.. autofunction:: blinkpy.helpers.util.http_req
|
||||
|
||||
.. autofunction:: blinkpy.helpers.util.attempt_reauthorization
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
API Reference
|
||||
----------------------
|
||||
|
||||
.. automodule:: blinkpy.api
|
||||
:members:
|
||||
|
||||
@@ -8,11 +8,16 @@ reports=no
|
||||
# too-many-* - are not enforced for the sake of readability
|
||||
# too-few-* - same as too-many-*
|
||||
# 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=
|
||||
format,
|
||||
bad-continuation,
|
||||
locally-disabled,
|
||||
unused-argument,
|
||||
duplicate-code,
|
||||
implicit-str-concat,
|
||||
too-many-arguments,
|
||||
too-many-branches,
|
||||
too-many-instance-attributes,
|
||||
@@ -23,3 +28,5 @@ disable=
|
||||
too-many-lines,
|
||||
too-few-public-methods,
|
||||
no-else-return,
|
||||
no-self-use,
|
||||
unexpected-keyword-arg,
|
||||
|
||||
+4
-2
@@ -1,2 +1,4 @@
|
||||
requests>=2.12.4
|
||||
testtools
|
||||
python-dateutil~=2.8.1
|
||||
requests~=2.23.0
|
||||
python-slugify~=4.0.0
|
||||
testtools==2.4.0
|
||||
|
||||
+11
-9
@@ -1,10 +1,12 @@
|
||||
flake8==3.5.0
|
||||
flake8-docstrings==1.3.0
|
||||
pylint==2.1.1
|
||||
pydocstyle==2.1.1
|
||||
pytest==3.7.1
|
||||
pytest-cov>=2.3.1
|
||||
pytest-sugar>=0.9.0
|
||||
pytest-timeout>=1.0.0
|
||||
black==19.10b0
|
||||
coverage==5.1
|
||||
flake8==3.7.9
|
||||
flake8-docstrings==1.5.0
|
||||
pylint==2.5.1
|
||||
pydocstyle==5.0.2
|
||||
pytest==5.4.1
|
||||
pytest-cov==2.8.1
|
||||
pytest-sugar>=0.9.2
|
||||
pytest-timeout>=1.3.3
|
||||
restructuredtext-lint>=1.0.1
|
||||
pygments>=2.2.0
|
||||
pygments>=2.2.0
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[flake8]
|
||||
exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build
|
||||
doctests = True
|
||||
# To work with Black
|
||||
max-line-length = 88
|
||||
|
||||
# E501: line too long
|
||||
# W503: Line break occurred before a binary operator
|
||||
# E203: Whitespace before ':'
|
||||
# D202 No blank lines allowed after function docstring
|
||||
# W504 line break after binary operator
|
||||
|
||||
ignore =
|
||||
E501,
|
||||
W503,
|
||||
E203,
|
||||
D202,
|
||||
W504,
|
||||
@@ -1,48 +1,41 @@
|
||||
"""Setup file for blinkpy."""
|
||||
# -*- coding: utf-8 -*-
|
||||
from os.path import abspath, dirname, join
|
||||
from os.path import abspath, dirname
|
||||
from setuptools import setup, find_packages
|
||||
# Fixes import error due to breaking change in pip 10
|
||||
# https://stackoverflow.com/a/49867265
|
||||
# pip > 10
|
||||
try:
|
||||
from pip._internal.req import parse_requirements
|
||||
# pip < 10
|
||||
except ImportError:
|
||||
from pip.req import parse_requirements
|
||||
from blinkpy.helpers.constants import (
|
||||
__version__, PROJECT_PACKAGE_NAME, PROJECT_LICENSE, PROJECT_URL,
|
||||
PROJECT_EMAIL, PROJECT_DESCRIPTION, PROJECT_CLASSIFIERS, PROJECT_AUTHOR,
|
||||
)
|
||||
|
||||
this_dir = abspath(dirname(__file__))
|
||||
PROJECT_VERSION = __version__
|
||||
|
||||
install_reqs = parse_requirements('{}/requirements.txt'.format(this_dir), session=False)
|
||||
test_reqs = parse_requirements('{}/requirements_test.txt'.format(this_dir), session=False)
|
||||
REQUIRES = [str(ir.req) for ir in install_reqs]
|
||||
TEST_REQUIRES = [str(ir.req) for ir in test_reqs]
|
||||
THIS_DIR = abspath(dirname(__file__))
|
||||
|
||||
REQUIRES = [
|
||||
"python-dateutil~=2.8.1",
|
||||
"requests~=2.23.0",
|
||||
"python-slugify~=4.0.0",
|
||||
]
|
||||
|
||||
PACKAGES = find_packages(exclude=['tests*', 'docs'])
|
||||
|
||||
with open('{}/README.rst'.format(this_dir), encoding='utf-8') as readme_file:
|
||||
long_description = readme_file.read()
|
||||
|
||||
name = PROJECT_PACKAGE_NAME
|
||||
version = __version__
|
||||
with open('{}/README.rst'.format(THIS_DIR), encoding='utf-8') as readme_file:
|
||||
LONG_DESCRIPTION = readme_file.read()
|
||||
|
||||
setup(
|
||||
name = PROJECT_PACKAGE_NAME,
|
||||
version = __version__,
|
||||
description = PROJECT_DESCRIPTION,
|
||||
long_description = long_description,
|
||||
author = PROJECT_AUTHOR,
|
||||
author_email = PROJECT_EMAIL,
|
||||
license = PROJECT_LICENSE,
|
||||
url = PROJECT_URL,
|
||||
platforms = 'any',
|
||||
py_modules = ['blinkpy'],
|
||||
name=PROJECT_PACKAGE_NAME,
|
||||
version=PROJECT_VERSION,
|
||||
description=PROJECT_DESCRIPTION,
|
||||
long_description=LONG_DESCRIPTION,
|
||||
author=PROJECT_AUTHOR,
|
||||
author_email=PROJECT_EMAIL,
|
||||
license=PROJECT_LICENSE,
|
||||
url=PROJECT_URL,
|
||||
platforms='any',
|
||||
py_modules=['blinkpy'],
|
||||
packages=PACKAGES,
|
||||
include_package_data = True,
|
||||
install_requires = REQUIRES,
|
||||
test_suite = 'tests',
|
||||
classifiers = PROJECT_CLASSIFIERS
|
||||
include_package_data=True,
|
||||
install_requires=REQUIRES,
|
||||
test_suite='tests',
|
||||
classifiers=PROJECT_CLASSIFIERS
|
||||
)
|
||||
|
||||
+19
-29
@@ -4,19 +4,11 @@ from blinkpy.helpers.util import BlinkURLHandler
|
||||
import blinkpy.helpers.constants as const
|
||||
|
||||
LOGIN_RESPONSE = {
|
||||
'region': {'mock': 'Test'},
|
||||
'networks': {
|
||||
'summary': {'name': 'TestNetwork'},
|
||||
'networks': [{
|
||||
'name': 'TestNetwork',
|
||||
'account_id': 1111,
|
||||
'id': 2222,
|
||||
'active': 'armed',
|
||||
'armed': True,
|
||||
'arm_string': 'Armed'
|
||||
}]
|
||||
},
|
||||
'authtoken': {'authtoken': 'foobar123', 'message': 'auth'}
|
||||
"region": {"mock": "Test"},
|
||||
"networks": {"1234": {"name": "test", "onboarded": True}},
|
||||
"authtoken": {"authtoken": "foobar123", "message": "auth"},
|
||||
"client": {"id": "5678"},
|
||||
"account": {"id": "1337"},
|
||||
}
|
||||
|
||||
|
||||
@@ -45,29 +37,29 @@ def mocked_session_send(*args, **kwargs):
|
||||
url = prepped.url
|
||||
header = prepped.headers
|
||||
method = prepped.method
|
||||
if method == 'GET':
|
||||
expected_token = LOGIN_RESPONSE['authtoken']['authtoken']
|
||||
if header['TOKEN_AUTH'] != expected_token:
|
||||
response = {'message': 'Not Authorized', 'code': 400}
|
||||
if method == "GET":
|
||||
expected_token = LOGIN_RESPONSE["authtoken"]["authtoken"]
|
||||
if header["TOKEN_AUTH"] != expected_token:
|
||||
response = {"message": "Not Authorized", "code": 400}
|
||||
status = 400
|
||||
elif url == 'use_bad_response':
|
||||
response = {'foo': 'bar'}
|
||||
elif url == "use_bad_response":
|
||||
response = {"foo": "bar"}
|
||||
status = 200
|
||||
elif url == 'reauth':
|
||||
response = {'message': 'REAUTH', 'code': 777}
|
||||
elif url == "reauth":
|
||||
response = {"message": "REAUTH", "code": 777}
|
||||
status = 777
|
||||
else:
|
||||
response = {'test': 'foo'}
|
||||
response = {"test": "foo"}
|
||||
status = 200
|
||||
elif method == 'POST':
|
||||
if url in (const.LOGIN_URL, const.LOGIN_BACKUP_URL):
|
||||
elif method == "POST":
|
||||
if url in const.LOGIN_URLS:
|
||||
response = LOGIN_RESPONSE
|
||||
status = 200
|
||||
elif url == 'http://wrong.url/' or url is None:
|
||||
response = {'message': 'Error', 'code': 404}
|
||||
elif url == "http://wrong.url/" or url is None:
|
||||
response = {"message": "Error", "code": 404}
|
||||
status = 404
|
||||
else:
|
||||
response = {'message': 'foo', 'code': 200}
|
||||
response = {"message": "foo", "code": 200}
|
||||
status = 200
|
||||
|
||||
return MockResponse(response, status)
|
||||
@@ -75,5 +67,3 @@ def mocked_session_send(*args, **kwargs):
|
||||
|
||||
class MockURLHandler(BlinkURLHandler):
|
||||
"""Mocks URL Handler in blinkpy module."""
|
||||
|
||||
pass
|
||||
|
||||
+104
-93
@@ -1,18 +1,15 @@
|
||||
"""Tests camera and system functions."""
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from requests import Request
|
||||
import logging
|
||||
|
||||
from blinkpy import blinkpy
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.camera import BlinkCamera
|
||||
from blinkpy.helpers.util import create_session
|
||||
from blinkpy.helpers.constants import BLINK_URL
|
||||
from blinkpy.helpers.util import create_session, get_time
|
||||
import tests.mock_responses as mresp
|
||||
|
||||
USERNAME = 'foobar'
|
||||
PASSWORD = 'deadbeef'
|
||||
USERNAME = "foobar"
|
||||
PASSWORD = "deadbeef"
|
||||
|
||||
|
||||
class MockSyncModule(BlinkSyncModule):
|
||||
@@ -20,7 +17,7 @@ class MockSyncModule(BlinkSyncModule):
|
||||
|
||||
def __init__(self, blink, header):
|
||||
"""Create mock sync module instance."""
|
||||
super().__init__(blink, header)
|
||||
super().__init__(blink, header, network_id=None, camera_list=None)
|
||||
self.blink = blink
|
||||
self.header = header
|
||||
self.return_value = None
|
||||
@@ -37,103 +34,117 @@ class MockSyncModule(BlinkSyncModule):
|
||||
return self.return_value
|
||||
|
||||
|
||||
@mock.patch('blinkpy.helpers.util.Session.send',
|
||||
side_effect=mresp.mocked_session_send)
|
||||
@mock.patch("blinkpy.helpers.util.Session.send", side_effect=mresp.mocked_session_send)
|
||||
class TestBlinkFunctions(unittest.TestCase):
|
||||
"""Test Blink and BlinkCamera functions in blinkpy."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up Blink module."""
|
||||
self.blink = blinkpy.Blink(username=USERNAME,
|
||||
password=PASSWORD)
|
||||
self.blink = blinkpy.Blink(username=USERNAME, password=PASSWORD)
|
||||
# pylint: disable=protected-access
|
||||
self.blink._auth_header = {
|
||||
'Host': 'test.url.tld',
|
||||
'TOKEN_AUTH': 'foobar123'
|
||||
}
|
||||
self.blink.urls = blinkpy.BlinkURLHandler('test')
|
||||
self.config = {
|
||||
'device_id': 1111,
|
||||
'name': 'foobar',
|
||||
'armed': False,
|
||||
'active': 'disabled',
|
||||
'thumbnail': '/test',
|
||||
'video': '/test.mp4',
|
||||
'temp': 80,
|
||||
'battery': 3,
|
||||
'notifications': 2,
|
||||
'region_id': 'test',
|
||||
'device_type': 'camera'
|
||||
}
|
||||
self.blink.sync = MockSyncModule(
|
||||
self.blink, self.blink._auth_header)
|
||||
self.blink._auth_header = {"Host": "test.url.tld", "TOKEN_AUTH": "foobar123"}
|
||||
self.blink.urls = blinkpy.BlinkURLHandler("test")
|
||||
self.blink.session = create_session()
|
||||
self.camera = BlinkCamera(self.config, self.blink.sync)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after test."""
|
||||
self.blink = None
|
||||
self.config = {}
|
||||
self.camera = None
|
||||
|
||||
def test_image_refresh(self, mock_sess):
|
||||
"""Test image refresh function."""
|
||||
self.blink.sync.return_value = {'devices': [self.config]}
|
||||
image = self.camera.image_refresh()
|
||||
self.assertEqual(image,
|
||||
'https://rest.test.{}/test.jpg'.format(BLINK_URL))
|
||||
|
||||
@mock.patch('blinkpy.sync_module.BlinkSyncModule.camera_config_request')
|
||||
@mock.patch('blinkpy.sync_module.BlinkSyncModule._video_request')
|
||||
def test_refresh(self, vid_req, req, mock_sess):
|
||||
"""Test blinkpy refresh function."""
|
||||
req.return_value = {'foo': 'bar'}
|
||||
self.blink.sync.cameras = {'foobar': self.camera}
|
||||
self.blink.sync.return_value = {'devices': [{'foo': 'bar'}]}
|
||||
# pylint: disable=protected-access
|
||||
summary = {'devices': [self.config]}
|
||||
# pylint: disable=protected-access
|
||||
events = {'foo': 'bar'}
|
||||
vid_req.return_value = [
|
||||
{
|
||||
'camera_name': 'foobar',
|
||||
'address': '/new.mp4',
|
||||
'thumbnail': '/new',
|
||||
}
|
||||
]
|
||||
with mock.patch('blinkpy.blinkpy.Blink.summary_request',
|
||||
return_value=summary):
|
||||
with mock.patch('blinkpy.blinkpy.Blink.events_request',
|
||||
return_value=events):
|
||||
self.blink.refresh_rate = 0
|
||||
self.blink.refresh()
|
||||
test_camera = self.blink.sync.cameras['foobar']
|
||||
self.assertEqual(test_camera.clip,
|
||||
'https://rest.test.{}/new.mp4'.format(BLINK_URL))
|
||||
self.assertEqual(test_camera.thumbnail,
|
||||
'https://rest.test.{}/new.jpg'.format(BLINK_URL))
|
||||
|
||||
def test_set_links(self, mock_sess):
|
||||
"""Test the link set method."""
|
||||
self.blink.sync.cameras = {'foobar': self.camera}
|
||||
self.blink.network_id = 9999
|
||||
self.blink.sync.set_links()
|
||||
net_url = "{}/{}".format(self.blink.urls.network_url, 9999)
|
||||
self.assertEqual(self.camera.image_link,
|
||||
"{}/camera/1111/thumbnail".format(net_url))
|
||||
self.assertEqual(self.camera.arm_link,
|
||||
"{}/camera/1111/".format(net_url))
|
||||
|
||||
@mock.patch('blinkpy.blinkpy.http_req')
|
||||
@mock.patch("blinkpy.login_handler.api.request_login")
|
||||
def test_backup_url(self, req, mock_sess):
|
||||
"""Test backup login method."""
|
||||
fake_req = Request('POST', 'http://wrong.url').prepare()
|
||||
req.side_effect = [
|
||||
mresp.mocked_session_send(fake_req),
|
||||
{'authtoken': {'authtoken': 'foobar123'}}
|
||||
]
|
||||
self.blink.get_auth_token()
|
||||
self.assertEqual(self.blink.region_id, 'piri')
|
||||
self.assertEqual(self.blink.region, 'UNKNOWN')
|
||||
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 = [bad_req, bad_req, new_req]
|
||||
self.blink.login_handler.login_urls = ["test1", "test2", "test3"]
|
||||
self.blink.login_handler.login(self.blink)
|
||||
self.assertEqual(self.blink.login_handler.login_url, "test3")
|
||||
|
||||
req.side_effect = [bad_req, new_req, bad_req]
|
||||
self.blink.login_handler.login_urls = ["test1", "test2", "test3"]
|
||||
self.blink.login_handler.login(self.blink)
|
||||
self.assertEqual(self.blink.login_handler.login_url, "test2")
|
||||
|
||||
def test_merge_cameras(self, mock_sess):
|
||||
"""Test merge camera functionality."""
|
||||
first_dict = {"foo": "bar", "test": 123}
|
||||
next_dict = {"foobar": 456, "bar": "foo"}
|
||||
self.blink.sync["foo"] = BlinkSyncModule(self.blink, "foo", 1, [])
|
||||
self.blink.sync["bar"] = BlinkSyncModule(self.blink, "bar", 2, [])
|
||||
self.blink.sync["foo"].cameras = first_dict
|
||||
self.blink.sync["bar"].cameras = next_dict
|
||||
result = self.blink.merge_cameras()
|
||||
expected = {"foo": "bar", "test": 123, "foobar": 456, "bar": "foo"}
|
||||
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
|
||||
self.assertEqual(self.blink._token, 'foobar123')
|
||||
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)
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
"""
|
||||
Test full system.
|
||||
|
||||
Tests the system initialization and attributes of
|
||||
the main Blink system. Tests if we properly catch
|
||||
any communication related errors at startup.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from blinkpy import blinkpy
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.helpers.util import (
|
||||
http_req, create_session, BlinkAuthenticationException,
|
||||
BlinkException, BlinkURLHandler)
|
||||
import tests.mock_responses as mresp
|
||||
|
||||
USERNAME = 'foobar'
|
||||
PASSWORD = 'deadbeef'
|
||||
|
||||
|
||||
@mock.patch('blinkpy.helpers.util.Session.send',
|
||||
side_effect=mresp.mocked_session_send)
|
||||
class TestBlinkSetup(unittest.TestCase):
|
||||
"""Test the Blink class in blinkpy."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up Blink module."""
|
||||
self.blink_no_cred = blinkpy.Blink()
|
||||
self.blink = blinkpy.Blink(username=USERNAME,
|
||||
password=PASSWORD)
|
||||
self.blink.sync = BlinkSyncModule(self.blink, dict(), self.blink.urls)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after test."""
|
||||
self.blink = None
|
||||
self.blink_no_cred = None
|
||||
|
||||
def test_initialization(self, mock_sess):
|
||||
"""Verify we can initialize blink."""
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(self.blink._username, USERNAME)
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(self.blink._password, PASSWORD)
|
||||
|
||||
def test_no_credentials(self, mock_sess):
|
||||
"""Check that we throw an exception when no username/password."""
|
||||
with self.assertRaises(BlinkAuthenticationException):
|
||||
self.blink_no_cred.get_auth_token()
|
||||
# pylint: disable=protected-access
|
||||
self.blink_no_cred._username = USERNAME
|
||||
with self.assertRaises(BlinkAuthenticationException):
|
||||
self.blink_no_cred.get_auth_token()
|
||||
|
||||
def test_no_auth_header(self, mock_sess):
|
||||
"""Check that we throw an exception when no auth header given."""
|
||||
# pylint: disable=unused-variable
|
||||
(region_id, region), = mresp.LOGIN_RESPONSE['region'].items()
|
||||
self.blink.urls = BlinkURLHandler(region_id)
|
||||
with self.assertRaises(BlinkException):
|
||||
self.blink.get_ids()
|
||||
with self.assertRaises(BlinkException):
|
||||
self.blink.summary_request()
|
||||
|
||||
@mock.patch('blinkpy.blinkpy.getpass.getpass')
|
||||
def test_manual_login(self, getpwd, mock_sess):
|
||||
"""Check that we can manually use the login() function."""
|
||||
getpwd.return_value = PASSWORD
|
||||
with mock.patch('builtins.input', return_value=USERNAME):
|
||||
self.assertTrue(self.blink_no_cred.login())
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(self.blink_no_cred._username, USERNAME)
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(self.blink_no_cred._password, PASSWORD)
|
||||
|
||||
def test_bad_request(self, mock_sess):
|
||||
"""Check that we raise an Exception with a bad request."""
|
||||
self.blink.session = create_session()
|
||||
with self.assertRaises(BlinkException):
|
||||
http_req(self.blink, reqtype='bad')
|
||||
|
||||
with self.assertRaises(BlinkAuthenticationException):
|
||||
http_req(self.blink, reqtype='post', is_retry=True)
|
||||
|
||||
def test_authentication(self, mock_sess):
|
||||
"""Check that we can authenticate Blink up properly."""
|
||||
authtoken = self.blink.get_auth_token()['TOKEN_AUTH']
|
||||
expected = mresp.LOGIN_RESPONSE['authtoken']['authtoken']
|
||||
self.assertEqual(authtoken, expected)
|
||||
|
||||
def test_reauthorization_attempt(self, mock_sess):
|
||||
"""Check that we can reauthorize after first unsuccessful attempt."""
|
||||
original_header = self.blink.get_auth_token()
|
||||
# pylint: disable=protected-access
|
||||
bad_header = {'Host': self.blink._host, 'TOKEN_AUTH': 'BADTOKEN'}
|
||||
# pylint: disable=protected-access
|
||||
self.blink._auth_header = bad_header
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(self.blink._auth_header, bad_header)
|
||||
self.blink.summary_request()
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(self.blink._auth_header, original_header)
|
||||
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
Test full system.
|
||||
|
||||
Tests the system initialization and attributes of
|
||||
the main Blink system. Tests if we properly catch
|
||||
any communication related errors at startup.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from blinkpy import api
|
||||
from blinkpy.blinkpy import Blink
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.login_handler import LoginHandler
|
||||
from blinkpy.helpers.util import (
|
||||
http_req,
|
||||
create_session,
|
||||
BlinkException,
|
||||
BlinkURLHandler,
|
||||
)
|
||||
from blinkpy.helpers.constants import __version__
|
||||
import tests.mock_responses as mresp
|
||||
|
||||
USERNAME = "foobar"
|
||||
PASSWORD = "deadbeef"
|
||||
|
||||
|
||||
@mock.patch("blinkpy.helpers.util.Session.send", side_effect=mresp.mocked_session_send)
|
||||
class TestBlinkSetup(unittest.TestCase):
|
||||
"""Test the Blink class in blinkpy."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up Blink module."""
|
||||
self.blink = Blink(username=USERNAME, password=PASSWORD)
|
||||
self.blink.sync["test"] = BlinkSyncModule(self.blink, "test", "1234", [])
|
||||
self.blink.urls = BlinkURLHandler("test")
|
||||
self.blink.session = create_session()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after test."""
|
||||
self.blink = None
|
||||
|
||||
def test_initialization(self, mock_sess):
|
||||
"""Verify we can initialize blink."""
|
||||
self.assertEqual(self.blink.version, __version__)
|
||||
self.assertEqual(self.blink.login_handler.data["username"], USERNAME)
|
||||
self.assertEqual(self.blink.login_handler.data["password"], PASSWORD)
|
||||
|
||||
def test_bad_request(self, mock_sess):
|
||||
"""Check that we raise an Exception with a bad request."""
|
||||
self.blink.session = create_session()
|
||||
explog = "WARNING:blinkpy.helpers.util:" "Response from server: 200 - foo"
|
||||
with self.assertRaises(BlinkException):
|
||||
http_req(self.blink, reqtype="bad")
|
||||
|
||||
with self.assertLogs() as logrecord:
|
||||
http_req(self.blink, reqtype="post", is_retry=True)
|
||||
self.assertEqual(logrecord.output, [explog])
|
||||
|
||||
def test_authentication(self, mock_sess):
|
||||
"""Check that we can authenticate Blink up properly."""
|
||||
authtoken = self.blink.get_auth_token()["TOKEN_AUTH"]
|
||||
expected = mresp.LOGIN_RESPONSE["authtoken"]["authtoken"]
|
||||
self.assertEqual(authtoken, expected)
|
||||
|
||||
def test_reauthorization_attempt(self, mock_sess):
|
||||
"""Check that we can reauthorize after first unsuccessful attempt."""
|
||||
original_header = self.blink.get_auth_token()
|
||||
# pylint: disable=protected-access
|
||||
bad_header = {"Host": self.blink._host, "TOKEN_AUTH": "BADTOKEN"}
|
||||
# pylint: disable=protected-access
|
||||
self.blink._auth_header = bad_header
|
||||
self.assertEqual(self.blink.auth_header, bad_header)
|
||||
api.request_homescreen(self.blink)
|
||||
self.assertEqual(self.blink.auth_header, original_header)
|
||||
|
||||
def test_multiple_networks(self, mock_sess):
|
||||
"""Check that we handle multiple networks appropriately."""
|
||||
self.blink.networks = {
|
||||
"0000": {"onboarded": False, "name": "foo"},
|
||||
"5678": {"onboarded": True, "name": "bar"},
|
||||
"1234": {"onboarded": False, "name": "test"},
|
||||
}
|
||||
self.blink.get_ids()
|
||||
self.assertTrue("5678" in self.blink.network_ids)
|
||||
|
||||
def test_multiple_onboarded_networks(self, mock_sess):
|
||||
"""Check that we handle multiple networks appropriately."""
|
||||
self.blink.networks = {
|
||||
"0000": {"onboarded": False, "name": "foo"},
|
||||
"5678": {"onboarded": True, "name": "bar"},
|
||||
"1234": {"onboarded": True, "name": "test"},
|
||||
}
|
||||
self.blink.get_ids()
|
||||
self.assertTrue("0000" not in self.blink.network_ids)
|
||||
self.assertTrue("5678" in self.blink.network_ids)
|
||||
self.assertTrue("1234" in self.blink.network_ids)
|
||||
|
||||
@mock.patch("blinkpy.blinkpy.time.time")
|
||||
def test_throttle(self, mock_time, mock_sess):
|
||||
"""Check throttling functionality."""
|
||||
now = self.blink.refresh_rate + 1
|
||||
mock_time.return_value = now
|
||||
self.assertEqual(self.blink.last_refresh, None)
|
||||
self.assertEqual(self.blink.check_if_ok_to_update(), True)
|
||||
self.assertEqual(self.blink.last_refresh, None)
|
||||
with mock.patch(
|
||||
"blinkpy.sync_module.BlinkSyncModule.refresh", return_value=True
|
||||
):
|
||||
self.blink.refresh()
|
||||
|
||||
self.assertEqual(self.blink.last_refresh, now)
|
||||
self.assertEqual(self.blink.check_if_ok_to_update(), False)
|
||||
self.assertEqual(self.blink.last_refresh, now)
|
||||
|
||||
def test_sync_case_insensitive_dict(self, mock_sess):
|
||||
"""Check that we can access sync modules ignoring case."""
|
||||
self.assertEqual(self.blink.sync["test"].name, "test")
|
||||
self.assertEqual(self.blink.sync["TEST"].name, "test")
|
||||
|
||||
@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, {})
|
||||
|
||||
@mock.patch.object(LoginHandler, "send_auth_key")
|
||||
@mock.patch.object(Blink, "setup_post_verify")
|
||||
def test_startup_prompt(self, mock_send_key, mock_verify, mock_sess):
|
||||
"""Test startup logic with command-line prompt."""
|
||||
mock_send_key.return_value = True
|
||||
mock_verify.return_value = True
|
||||
self.blink.no_prompt = False
|
||||
self.blink.key_required = True
|
||||
self.blink.available = True
|
||||
with mock.patch("builtins.input", return_value="1234"):
|
||||
self.blink.start()
|
||||
self.assertFalse(self.blink.key_required)
|
||||
|
||||
def test_startup_no_prompt(self, mock_sess):
|
||||
"""Test startup with no_prompt flag set."""
|
||||
self.blink.key_required = True
|
||||
self.blink.no_prompt = True
|
||||
self.blink.start()
|
||||
self.assertTrue(self.blink.key_required)
|
||||
+112
-173
@@ -12,204 +12,143 @@ from blinkpy import blinkpy
|
||||
from blinkpy.helpers.util import create_session, BlinkURLHandler
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.camera import BlinkCamera
|
||||
from blinkpy.helpers.constants import BLINK_URL
|
||||
import tests.mock_responses as mresp
|
||||
|
||||
USERNAME = 'foobar'
|
||||
PASSWORD = 'deadbeef'
|
||||
USERNAME = "foobar"
|
||||
PASSWORD = "deadbeef"
|
||||
|
||||
CAMERA_CFG = {
|
||||
'camera': [
|
||||
"camera": [
|
||||
{
|
||||
'battery_voltage': 90,
|
||||
'motion_alert': True,
|
||||
'wifi_strength': -30,
|
||||
'temperature': 68
|
||||
"battery_voltage": 90,
|
||||
"motion_alert": True,
|
||||
"wifi_strength": -30,
|
||||
"temperature": 68,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@mock.patch('blinkpy.helpers.util.Session.send',
|
||||
side_effect=mresp.mocked_session_send)
|
||||
@mock.patch("blinkpy.helpers.util.Session.send", side_effect=mresp.mocked_session_send)
|
||||
class TestBlinkCameraSetup(unittest.TestCase):
|
||||
"""Test the Blink class in blinkpy."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up Blink module."""
|
||||
self.blink = blinkpy.Blink(username=USERNAME,
|
||||
password=PASSWORD)
|
||||
self.camera_config = {
|
||||
'device_id': 1111,
|
||||
'name': 'foobar',
|
||||
'armed': False,
|
||||
'active': 'disarmed',
|
||||
'thumbnail': '/test/image',
|
||||
'video': '/test/clip/clip.mp4',
|
||||
'temp': 70,
|
||||
'battery': 3,
|
||||
'notifications': 2,
|
||||
'region_id': 'test'
|
||||
}
|
||||
|
||||
self.blink = blinkpy.Blink(username=USERNAME, password=PASSWORD)
|
||||
header = {
|
||||
'Host': 'abc.zxc',
|
||||
'TOKEN_AUTH': mresp.LOGIN_RESPONSE['authtoken']['authtoken']
|
||||
"Host": "abc.zxc",
|
||||
"TOKEN_AUTH": mresp.LOGIN_RESPONSE["authtoken"]["authtoken"],
|
||||
}
|
||||
# pylint: disable=protected-access
|
||||
self.blink._auth_header = header
|
||||
self.blink.session = create_session()
|
||||
self.blink.urls = BlinkURLHandler('test')
|
||||
self.blink.network_id = '0000'
|
||||
self.sync = BlinkSyncModule(self.blink, header, self.blink.urls)
|
||||
self.blink.urls = BlinkURLHandler("test")
|
||||
self.blink.sync["test"] = BlinkSyncModule(self.blink, "test", 1234, [])
|
||||
self.camera = BlinkCamera(self.blink.sync["test"])
|
||||
self.camera.name = "foobar"
|
||||
self.blink.sync["test"].cameras["foobar"] = self.camera
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after test."""
|
||||
self.blink = None
|
||||
|
||||
@mock.patch('blinkpy.sync_module.BlinkSyncModule.camera_config_request',
|
||||
return_value=CAMERA_CFG)
|
||||
def test_camera_properties(self, mock_cfg, mock_sess):
|
||||
"""Tests all property set/recall."""
|
||||
self.blink.urls = BlinkURLHandler('test')
|
||||
|
||||
self.sync.cameras = {
|
||||
'foobar': BlinkCamera(self.camera_config, self.sync)
|
||||
def test_camera_update(self, mock_sess):
|
||||
"""Test that we can properly update camera properties."""
|
||||
config = {
|
||||
"name": "new",
|
||||
"id": 1234,
|
||||
"network_id": 5678,
|
||||
"serial": "12345678",
|
||||
"enabled": False,
|
||||
"battery_voltage": 90,
|
||||
"battery_state": "ok",
|
||||
"temperature": 68,
|
||||
"wifi_strength": 4,
|
||||
"thumbnail": "/thumb",
|
||||
}
|
||||
|
||||
for name in self.sync.cameras:
|
||||
camera = self.sync.cameras[name]
|
||||
camera.update(self.camera_config, skip_cache=True)
|
||||
self.assertEqual(camera.id, '1111')
|
||||
self.assertEqual(camera.name, 'foobar')
|
||||
self.assertEqual(camera.armed, False)
|
||||
self.assertEqual(
|
||||
camera.thumbnail,
|
||||
"https://rest.test.{}/test/image.jpg".format(BLINK_URL)
|
||||
)
|
||||
self.assertEqual(
|
||||
camera.clip,
|
||||
"https://rest.test.{}/test/clip/clip.mp4".format(BLINK_URL)
|
||||
)
|
||||
self.assertEqual(camera.temperature, 68)
|
||||
self.assertEqual(camera.temperature_c, 20.0)
|
||||
self.assertEqual(camera.battery, 50)
|
||||
self.assertEqual(camera.battery_string, "OK")
|
||||
self.assertEqual(camera.notifications, 2)
|
||||
self.assertEqual(camera.region_id, 'test')
|
||||
self.assertEqual(camera.motion_enabled, True)
|
||||
self.assertEqual(camera.wifi_strength, -30)
|
||||
|
||||
camera_config = self.camera_config
|
||||
camera_config['active'] = 'armed'
|
||||
camera_config['thumbnail'] = '/test2/image'
|
||||
camera_config['video'] = '/test2/clip.mp4'
|
||||
camera_config['temp'] = 60
|
||||
camera_config['battery'] = 0
|
||||
camera_config['notifications'] = 4
|
||||
for name in self.sync.cameras:
|
||||
camera = self.sync.cameras[name]
|
||||
camera.update(camera_config, skip_cache=True)
|
||||
self.assertEqual(camera.armed, True)
|
||||
self.assertEqual(
|
||||
camera.thumbnail,
|
||||
"https://rest.test.{}/test2/image.jpg".format(BLINK_URL)
|
||||
)
|
||||
self.assertEqual(
|
||||
camera.clip,
|
||||
"https://rest.test.{}/test2/clip.mp4".format(BLINK_URL)
|
||||
)
|
||||
self.assertEqual(camera.temperature, 68)
|
||||
self.assertEqual(camera.battery, 50)
|
||||
self.assertEqual(camera.battery_string, "Low")
|
||||
self.assertEqual(camera.notifications, 4)
|
||||
camera_config['battery'] = -10
|
||||
camera.update(camera_config, skip_cache=True)
|
||||
self.assertEqual(camera.battery_string, "Unknown")
|
||||
|
||||
def test_camera_case(self, mock_sess):
|
||||
"""Tests camera case sensitivity."""
|
||||
camera_object = BlinkCamera(self.camera_config, self.sync)
|
||||
self.sync.cameras['foobar'] = camera_object
|
||||
self.assertEqual(camera_object, self.sync.cameras['fOoBaR'])
|
||||
|
||||
@mock.patch('blinkpy.sync_module.BlinkSyncModule.camera_config_request',
|
||||
return_value=CAMERA_CFG)
|
||||
def test_camera_attributes(self, mock_cfg, mock_sess):
|
||||
"""Tests camera attributes."""
|
||||
self.blink.urls = BlinkURLHandler('test')
|
||||
|
||||
self.sync.cameras = {
|
||||
'foobar': BlinkCamera(self.camera_config, self.sync)
|
||||
self.camera.last_record = ["1"]
|
||||
self.camera.sync.last_record = {
|
||||
"new": {"clip": "/test.mp4", "time": "1970-01-01T00:00:00"}
|
||||
}
|
||||
mock_sess.side_effect = [
|
||||
mresp.MockResponse({"temp": 71}, 200),
|
||||
"test",
|
||||
"foobar",
|
||||
]
|
||||
self.camera.update(config)
|
||||
self.assertEqual(self.camera.name, "new")
|
||||
self.assertEqual(self.camera.camera_id, "1234")
|
||||
self.assertEqual(self.camera.network_id, "5678")
|
||||
self.assertEqual(self.camera.serial, "12345678")
|
||||
self.assertEqual(self.camera.motion_enabled, False)
|
||||
self.assertEqual(self.camera.battery, "ok")
|
||||
self.assertEqual(self.camera.temperature, 68)
|
||||
self.assertEqual(self.camera.temperature_c, 20)
|
||||
self.assertEqual(self.camera.temperature_calibrated, 71)
|
||||
self.assertEqual(self.camera.wifi_strength, 4)
|
||||
self.assertEqual(
|
||||
self.camera.thumbnail, "https://rest-test.immedia-semi.com/thumb.jpg"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.camera.clip, "https://rest-test.immedia-semi.com/test.mp4"
|
||||
)
|
||||
self.assertEqual(self.camera.image_from_cache, "test")
|
||||
self.assertEqual(self.camera.video_from_cache, "foobar")
|
||||
|
||||
for name in self.sync.cameras:
|
||||
camera = self.sync.cameras[name]
|
||||
camera.update(self.camera_config, skip_cache=True)
|
||||
camera_attr = camera.attributes
|
||||
self.assertEqual(camera_attr['device_id'], '1111')
|
||||
self.assertEqual(camera_attr['name'], 'foobar')
|
||||
self.assertEqual(camera_attr['armed'], False)
|
||||
self.assertEqual(
|
||||
camera_attr['thumbnail'],
|
||||
"https://rest.test.{}/test/image.jpg".format(BLINK_URL)
|
||||
)
|
||||
self.assertEqual(
|
||||
camera_attr['video'],
|
||||
"https://rest.test.{}/test/clip/clip.mp4".format(BLINK_URL)
|
||||
)
|
||||
self.assertEqual(camera_attr['temperature'], 68)
|
||||
self.assertEqual(camera_attr['temperature_c'], 20.0)
|
||||
self.assertEqual(camera_attr['battery'], 50)
|
||||
self.assertEqual(camera_attr['notifications'], 2)
|
||||
self.assertEqual(camera_attr['network_id'], '0000')
|
||||
self.assertEqual(camera_attr['motion_enabled'], True)
|
||||
self.assertEqual(camera_attr['wifi_strength'], -30)
|
||||
|
||||
@mock.patch('blinkpy.camera.BlinkCamera.image_refresh',
|
||||
return_value='https://fake.url')
|
||||
def test_camera_cache(self, img_refresh, mock_sess):
|
||||
"""Tests camera cache."""
|
||||
update_vals = {
|
||||
'name': 'foobar',
|
||||
'active': 'disabled',
|
||||
'video': '/clip.mp4',
|
||||
'thumbnail': '/image',
|
||||
'battery': 3,
|
||||
'notifications': 1,
|
||||
def test_no_thumbnails(self, mock_sess):
|
||||
"""Tests that thumbnail is 'None' if none found."""
|
||||
mock_sess.return_value = "foobar"
|
||||
self.camera.last_record = ["1"]
|
||||
config = {
|
||||
"name": "new",
|
||||
"id": 1234,
|
||||
"network_id": 5678,
|
||||
"serial": "12345678",
|
||||
"enabled": False,
|
||||
"battery_voltage": 90,
|
||||
"battery_state": "ok",
|
||||
"temperature": 68,
|
||||
"wifi_strength": 4,
|
||||
"thumbnail": "",
|
||||
}
|
||||
self.sync.cameras = {
|
||||
'foobar': BlinkCamera(self.camera_config, self.sync)
|
||||
self.camera.sync.homescreen = {"devices": []}
|
||||
self.assertEqual(self.camera.temperature_calibrated, None)
|
||||
with self.assertLogs() as logrecord:
|
||||
self.camera.update(config, force=True)
|
||||
self.assertEqual(self.camera.thumbnail, None)
|
||||
self.assertEqual(self.camera.last_record, ["1"])
|
||||
self.assertEqual(self.camera.temperature_calibrated, 68)
|
||||
self.assertEqual(
|
||||
logrecord.output,
|
||||
[
|
||||
(
|
||||
"WARNING:blinkpy.camera:Could not retrieve calibrated "
|
||||
"temperature."
|
||||
),
|
||||
(
|
||||
"WARNING:blinkpy.camera:Could not find thumbnail for camera new"
|
||||
"\nNoneType: None"
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def test_no_video_clips(self, mock_sess):
|
||||
"""Tests that we still proceed with camera setup with no videos."""
|
||||
mock_sess.return_value = "foobar"
|
||||
config = {
|
||||
"name": "new",
|
||||
"id": 1234,
|
||||
"network_id": 5678,
|
||||
"serial": "12345678",
|
||||
"enabled": False,
|
||||
"battery_voltage": 90,
|
||||
"battery_state": "ok",
|
||||
"temperature": 68,
|
||||
"wifi_strength": 4,
|
||||
"thumbnail": "/foobar",
|
||||
}
|
||||
|
||||
test_image = 'https://rest.test.immedia-semi.com/image.jpg'
|
||||
test_clip = 'https://rest.test.immedia-semi.com/clip.mp4'
|
||||
|
||||
for name, camera in self.sync.cameras.items():
|
||||
# Check that no cache returns None
|
||||
self.assertEqual(camera.name, name)
|
||||
self.assertEqual(camera.image_from_cache, None)
|
||||
self.assertEqual(camera.video_from_cache, None)
|
||||
|
||||
# Now, call an update with a new thumbnail to see if we update
|
||||
self.sync.records = []
|
||||
# pylint: disable=protected-access
|
||||
camera.update(update_vals)
|
||||
self.assertEqual(camera.thumbnail, test_image)
|
||||
self.assertEqual(camera.image_from_cache.status_code, 200)
|
||||
|
||||
# Now update the clip
|
||||
self.sync.record_dates = {camera.name: ['7', '1', '4', '3']}
|
||||
self.assertEqual(camera.last_record, list())
|
||||
camera.update(update_vals)
|
||||
self.assertEqual(camera.clip, test_clip)
|
||||
self.assertEqual(camera.last_record, list('7'))
|
||||
# First update should be false
|
||||
self.assertEqual(camera.motion_detected, False)
|
||||
self.sync.record_dates[camera.name].append('88')
|
||||
camera.update(update_vals)
|
||||
self.assertEqual(camera.last_record, ['88', '7'])
|
||||
self.assertEqual(camera.motion_detected, True)
|
||||
self.assertEqual(camera.video_from_cache.status_code, 200)
|
||||
# Next update shouldn't change records, and motion_dected=False
|
||||
camera.update(update_vals)
|
||||
self.assertEqual(camera.motion_detected, False)
|
||||
self.assertEqual(camera.video_from_cache.status_code, 200)
|
||||
self.camera.sync.homescreen = {"devices": []}
|
||||
self.camera.update(config, force_cache=True)
|
||||
self.assertEqual(self.camera.clip, None)
|
||||
self.assertEqual(self.camera.video_from_cache, None)
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
"""Test login handler."""
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from blinkpy.login_handler import LoginHandler
|
||||
import tests.mock_responses as mresp
|
||||
|
||||
USERNAME = "foobar"
|
||||
PASSWORD = "deadbeef"
|
||||
|
||||
|
||||
@mock.patch("blinkpy.helpers.util.Session.send", side_effect=mresp.mocked_session_send)
|
||||
class TestLoginHandler(unittest.TestCase):
|
||||
"""Test the LoginHandler class in blinkpy."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up Login Handler."""
|
||||
self.login_handler = LoginHandler()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after test."""
|
||||
self.login_handler = None
|
||||
|
||||
@mock.patch("blinkpy.login_handler.getpass")
|
||||
def test_manual_login(self, getpwd, mock_sess):
|
||||
"""Check that we can manually use the login() function."""
|
||||
getpwd.return_value = PASSWORD
|
||||
with mock.patch("builtins.input", return_value=USERNAME):
|
||||
self.assertTrue(self.login_handler.check_login())
|
||||
self.assertEqual(self.login_handler.data["username"], USERNAME)
|
||||
self.assertEqual(self.login_handler.data["password"], PASSWORD)
|
||||
|
||||
def test_no_cred_file(self, mock_sess):
|
||||
"""Check that we return false when cred file doesn't exist."""
|
||||
self.login_handler.cred_file = "/tmp/fake.file"
|
||||
self.assertFalse(self.login_handler.check_cred_file())
|
||||
|
||||
@mock.patch("blinkpy.login_handler.isfile")
|
||||
def test_exit_on_missing_json(self, mockisfile, mock_sess):
|
||||
"""Test that we fail on missing json data."""
|
||||
self.login_handler.cred_file = "/tmp/fake.file"
|
||||
mockisfile.return_value = True
|
||||
with mock.patch("builtins.open", mock.mock_open(read_data="{}")):
|
||||
self.assertFalse(self.login_handler.check_cred_file())
|
||||
|
||||
@mock.patch("blinkpy.login_handler.json.load")
|
||||
@mock.patch("blinkpy.login_handler.isfile")
|
||||
def test_cred_file(self, mockisfile, mockjson, mock_sess):
|
||||
"""Test that loading credential file works."""
|
||||
self.login_handler.cred_file = "/tmp/fake.file"
|
||||
mockjson.return_value = {"username": "foo", "password": "bar"}
|
||||
mockisfile.return_value = True
|
||||
with mock.patch("builtins.open", mock.mock_open(read_data="")):
|
||||
self.assertTrue(self.login_handler.check_cred_file())
|
||||
self.assertEqual(self.login_handler.data["username"], "foo")
|
||||
self.assertEqual(self.login_handler.data["password"], "bar")
|
||||
|
||||
def test_bad_response(self, mock_sess):
|
||||
"""Check bad response from server."""
|
||||
self.assertFalse(self.login_handler.validate_response(None, None))
|
||||
|
||||
def test_bad_response_code(self, mock_sess):
|
||||
"""Check bad response code from server."""
|
||||
fake_resp = mresp.MockResponse(None, 404)
|
||||
self.assertFalse(self.login_handler.validate_response(None, fake_resp))
|
||||
|
||||
def test_good_response_code(self, mock_sess):
|
||||
"""Check good response code from server."""
|
||||
fake_resp = mresp.MockResponse(None, 200)
|
||||
self.assertTrue(self.login_handler.validate_response(None, fake_resp))
|
||||
|
||||
@mock.patch("blinkpy.login_handler.util.gen_uid")
|
||||
def test_check_keys_no_persist(self, mock_uid, mock_sess):
|
||||
"""Check key generation."""
|
||||
uid_value = "abc123"
|
||||
mock_uid.return_value = "abc123"
|
||||
self.login_handler.persist_key = None
|
||||
data = self.login_handler.check_keys()
|
||||
self.assertEqual(data["uid"], uid_value)
|
||||
self.assertEqual(data["notification_key"], uid_value)
|
||||
|
||||
@mock.patch("blinkpy.login_handler.util.gen_uid")
|
||||
@mock.patch("blinkpy.login_handler.json.load")
|
||||
@mock.patch("blinkpy.login_handler.isfile")
|
||||
def test_check_keys_persist(self, mockisfile, mockjson, mock_uid, mock_sess):
|
||||
"""Check key load from file."""
|
||||
uid_value = "abc123"
|
||||
mock_file = {"uid": "321cba", "notification_key": "foobar123"}
|
||||
mock_uid.return_value = uid_value
|
||||
mockjson.return_value = mock_file
|
||||
mockisfile.return_value = True
|
||||
self.login_handler.persist_key = True
|
||||
data = self.login_handler.check_keys()
|
||||
self.assertEqual(mock_file["uid"], data["uid"])
|
||||
self.assertEqual(mock_file["notification_key"], data["notification_key"])
|
||||
|
||||
def test_check_key_required(self, mock_sess):
|
||||
"""Check key required method."""
|
||||
response_true = {"client": {"verification_required": True}}
|
||||
response_false = {"client": {"verification_required": False}}
|
||||
response_nokey = {}
|
||||
|
||||
mock_blink = MockBlink(response_nokey)
|
||||
self.assertFalse(self.login_handler.check_key_required(mock_blink))
|
||||
|
||||
mock_blink = MockBlink(response_false)
|
||||
self.assertFalse(self.login_handler.check_key_required(mock_blink))
|
||||
|
||||
mock_blink = MockBlink(response_true)
|
||||
self.assertTrue(self.login_handler.check_key_required(mock_blink))
|
||||
|
||||
@mock.patch("blinkpy.login_handler.api.request_verify")
|
||||
def test_send_auth_key(self, mock_req, mock_sess):
|
||||
"""Check sending of auth key."""
|
||||
mock_blink = MockBlink(None)
|
||||
mock_req.return_value = mresp.MockResponse({"valid": True}, 200)
|
||||
self.assertTrue(self.login_handler.send_auth_key(mock_blink, 1234))
|
||||
self.assertTrue(mock_blink.available)
|
||||
|
||||
mock_req.return_value = mresp.MockResponse(None, 200)
|
||||
self.assertFalse(self.login_handler.send_auth_key(mock_blink, 1234))
|
||||
|
||||
mock_req.return_value = mresp.MockResponse({}, 200)
|
||||
self.assertFalse(self.login_handler.send_auth_key(mock_blink, 1234))
|
||||
|
||||
self.assertTrue(self.login_handler.send_auth_key(mock_blink, None))
|
||||
|
||||
|
||||
class MockBlink:
|
||||
"""Object to mock basic blink class."""
|
||||
|
||||
def __init__(self, login_response):
|
||||
"""Initialize mock blink class."""
|
||||
self.available = False
|
||||
self.login_response = login_response
|
||||
+202
-83
@@ -6,102 +6,221 @@ from blinkpy import blinkpy
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.camera import BlinkCamera
|
||||
|
||||
USERNAME = 'foobar'
|
||||
PASSWORD = 'deadbeef'
|
||||
|
||||
|
||||
class MockSyncModule(BlinkSyncModule):
|
||||
"""Mock http requests from sync module."""
|
||||
|
||||
def __init__(self, blink, header):
|
||||
"""Create mock sync module instance."""
|
||||
super().__init__(blink, header)
|
||||
self.blink = blink
|
||||
self.header = header
|
||||
self.return_value = None
|
||||
self.return_value2 = None
|
||||
|
||||
def http_get(self, url, stream=False, json=True):
|
||||
"""Mock get request."""
|
||||
if stream and self.return_value2 is not None:
|
||||
return self.return_value2
|
||||
return self.return_value
|
||||
|
||||
def http_post(self, url):
|
||||
"""Mock post request."""
|
||||
return self.return_value
|
||||
USERNAME = "foobar"
|
||||
PASSWORD = "deadbeef"
|
||||
|
||||
|
||||
@mock.patch("blinkpy.api.http_req")
|
||||
class TestBlinkSyncModule(unittest.TestCase):
|
||||
"""Test BlinkSyncModule functions in blinkpy."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up Blink module."""
|
||||
self.blink = blinkpy.Blink(username=USERNAME,
|
||||
password=PASSWORD)
|
||||
self.blink = blinkpy.Blink(
|
||||
username=USERNAME, password=PASSWORD, motion_interval=0
|
||||
)
|
||||
# pylint: disable=protected-access
|
||||
self.blink._auth_header = {
|
||||
'Host': 'test.url.tld',
|
||||
'TOKEN_AUTH': 'foobar123'
|
||||
}
|
||||
self.blink.urls = blinkpy.BlinkURLHandler('test')
|
||||
self.config = {
|
||||
'device_id': 1111,
|
||||
'name': 'foobar',
|
||||
'armed': False,
|
||||
'active': 'disabled',
|
||||
'thumbnail': '/test',
|
||||
'video': '/test.mp4',
|
||||
'temp': 80,
|
||||
'battery': 3,
|
||||
'notifications': 2,
|
||||
'region_id': 'test',
|
||||
'device_type': 'camera'
|
||||
}
|
||||
self.blink.sync = MockSyncModule(
|
||||
self.blink, self.blink._auth_header)
|
||||
|
||||
self.camera = BlinkCamera(self.config, self.blink.sync)
|
||||
self.blink._auth_header = {"Host": "test.url.tld", "TOKEN_AUTH": "foobar123"}
|
||||
self.blink.last_refresh = 0
|
||||
self.blink.urls = blinkpy.BlinkURLHandler("test")
|
||||
self.blink.sync["test"] = BlinkSyncModule(self.blink, "test", "1234", [])
|
||||
self.camera = BlinkCamera(self.blink.sync)
|
||||
self.mock_start = [
|
||||
{
|
||||
"syncmodule": {
|
||||
"id": 1234,
|
||||
"network_id": 5678,
|
||||
"serial": "12345678",
|
||||
"status": "foobar",
|
||||
}
|
||||
},
|
||||
{"event": True},
|
||||
{},
|
||||
{},
|
||||
None,
|
||||
{"devicestatus": {}},
|
||||
]
|
||||
self.blink.sync["test"].network_info = {"network": {"armed": True}}
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after test."""
|
||||
self.blink = None
|
||||
self.config = {}
|
||||
self.camera = None
|
||||
self.mock_start = None
|
||||
|
||||
def test_get_videos(self):
|
||||
"""Test video access."""
|
||||
self.blink.sync.return_value = [
|
||||
{
|
||||
'camera_name': 'foobar',
|
||||
'address': '/new/test.mp4',
|
||||
'thumbnail': '/test/thumb'
|
||||
}
|
||||
]
|
||||
self.blink.sync.get_videos()
|
||||
self.assertEqual(self.blink.sync.videos['foobar'][0]['clip'],
|
||||
'/new/test.mp4')
|
||||
self.assertEqual(self.blink.sync.videos['foobar'][0]['thumb'],
|
||||
'/test/thumb')
|
||||
def test_get_events(self, mock_resp):
|
||||
"""Test get events function."""
|
||||
mock_resp.return_value = {"event": True}
|
||||
self.assertEqual(self.blink.sync["test"].get_events(), True)
|
||||
|
||||
@mock.patch('blinkpy.blinkpy.Blink.refresh')
|
||||
@mock.patch('blinkpy.sync_module.BlinkSyncModule._summary_request')
|
||||
@mock.patch('blinkpy.sync_module.BlinkSyncModule._video_request')
|
||||
def test_get_cameras(self, vid_req, req, refresh):
|
||||
"""Test camera extraction."""
|
||||
refresh.return_value = True
|
||||
req.return_value = {'devices': [self.config]}
|
||||
vid_req.return_value = [
|
||||
{
|
||||
'camera_name': 'foobar',
|
||||
'address': '/new.mp4',
|
||||
'thumbnail': '/new'
|
||||
}
|
||||
]
|
||||
self.blink.sync.get_cameras()
|
||||
self.assertTrue('foobar' in self.blink.sync.cameras)
|
||||
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_video_count(self):
|
||||
"""Test video count function."""
|
||||
self.blink.sync.return_value = {'count': 1}
|
||||
self.assertEqual(self.blink.sync.video_count, 1)
|
||||
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.assertEqual(self.blink.sync["test"].name, "test")
|
||||
self.assertEqual(self.blink.sync["test"].sync_id, 1234)
|
||||
self.assertEqual(self.blink.sync["test"].network_id, 5678)
|
||||
self.assertEqual(self.blink.sync["test"].serial, "12345678")
|
||||
self.assertEqual(self.blink.sync["test"].status, "foobar")
|
||||
|
||||
def test_unexpected_summary(self, mock_resp):
|
||||
"""Test unexpected summary response."""
|
||||
self.mock_start[0] = None
|
||||
mock_resp.side_effect = self.mock_start
|
||||
self.assertFalse(self.blink.sync["test"].start())
|
||||
|
||||
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,116 @@
|
||||
"""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))
|
||||
@@ -1,5 +1,5 @@
|
||||
[tox]
|
||||
envlist = build, py34, py35, py36, py37, lint
|
||||
envlist = build, py36, py37, py38, lint
|
||||
skip_missing_interpreters = True
|
||||
skipsdist = True
|
||||
|
||||
@@ -8,21 +8,39 @@ setenv =
|
||||
LANG=en_US.UTF-8
|
||||
PYTHONPATH = {toxinidir}
|
||||
commands =
|
||||
py.test --timeout=30 --duration=10 --cov=blinkpy --cov-report term-missing {posargs}
|
||||
pytest --timeout=9 --durations=10 --cov=blinkpy --cov-report term-missing {posargs}
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/requirements_test.txt
|
||||
|
||||
[testenv:lint]
|
||||
|
||||
[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:pylint]
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/requirements_test.txt
|
||||
basepython = python3
|
||||
ignore_errors = True
|
||||
commands =
|
||||
pylint --rcfile={toxinidir}/pylintrc blinkpy tests
|
||||
flake8 blinkpy tests
|
||||
pydocstyle blinkpy tests
|
||||
pylint --rcfile={toxinidir}/pylintrc blinkpy tests app
|
||||
|
||||
[testenv:lint]
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/requirements_test.txt
|
||||
basepython = python3
|
||||
commands =
|
||||
flake8 blinkpy tests app
|
||||
pydocstyle blinkpy tests app
|
||||
black --check --diff blinkpy tests app
|
||||
rst-lint README.rst
|
||||
rst-lint CHANGES.rst
|
||||
|
||||
|
||||
Reference in New Issue
Block a user