Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07e1759819 | ||
|
|
8fac6c5bc0 | ||
|
|
bd4307af58 | ||
|
|
bd6e3bb4e3 | ||
|
|
c2630a43e1 | ||
|
|
2e6fb97cc5 | ||
|
|
674dae4440 | ||
|
|
a1082812bb | ||
|
|
9d838fe4eb | ||
|
|
fe0aef69e4 | ||
|
|
1b1235210e | ||
|
|
451d725946 | ||
|
|
9d5ae9552a | ||
|
|
c3c558e934 | ||
|
|
a570413e66 | ||
|
|
3e23374b32 | ||
|
|
1c6a5eb6c6 | ||
|
|
40e9e4bd6c | ||
|
|
e1ff851068 | ||
|
|
8a4e507f22 | ||
|
|
cd7b5bfabc | ||
|
|
346882f1ff | ||
|
|
5bc2466da2 | ||
|
|
672f59ab4e | ||
|
|
ae26a9e371 | ||
|
|
37fc047174 | ||
|
|
6343965bbc | ||
|
|
411da60bb2 | ||
|
|
72c36d8381 | ||
|
|
b0974c1ab2 | ||
|
|
a76eac085a | ||
|
|
5319714a37 | ||
|
|
5d9105655f | ||
|
|
b98c03b96b | ||
|
|
ec703ae4b0 | ||
|
|
9ce09b4761 | ||
|
|
6d002fc8b0 | ||
|
|
7228340677 | ||
|
|
9335a37feb | ||
|
|
371348533f | ||
|
|
26f6defe60 | ||
|
|
52bc4af1f0 | ||
|
|
8c94305804 | ||
|
|
0eb57698bd | ||
|
|
7b0db80edd | ||
|
|
0e63e97a57 | ||
|
|
e4a21964d2 | ||
|
|
39ad37f23b | ||
|
|
34bc61e1c3 | ||
|
|
e6a7c367b3 | ||
|
|
66afd33b79 | ||
|
|
cf617d720e | ||
|
|
4f2eca08e0 | ||
|
|
2972780c18 | ||
|
|
9c3f3f2f98 | ||
|
|
0d83a7fcb1 | ||
|
|
80f4ca7c4f | ||
|
|
96e7fce304 | ||
|
|
13a358c07c | ||
|
|
389745533c | ||
|
|
65dd547a32 | ||
|
|
91a877da79 | ||
|
|
d357215e0e | ||
|
|
ff47dc4731 | ||
|
|
fedea74256 | ||
|
|
66a6701a66 | ||
|
|
5bdfd6da52 | ||
|
|
a8f90400b2 | ||
|
|
3348386a4c | ||
|
|
1b53c4a0dd | ||
|
|
24158638ef | ||
|
|
901cbfc9d1 | ||
|
|
9f85068a0d | ||
|
|
90b83544f4 | ||
|
|
1d8e8e53d9 | ||
|
|
46a0a433ec | ||
|
|
d38d30471a | ||
|
|
5aad8ea641 | ||
|
|
74f4bd7763 | ||
|
|
51dd69a51f | ||
|
|
950e7d4971 | ||
|
|
623b694c78 | ||
|
|
7162ea24cc | ||
|
|
968b77b8bd | ||
|
|
6e00b126d1 | ||
|
|
003ceb0944 | ||
|
|
ce6dec50cd | ||
|
|
87643ddcab | ||
|
|
017d8aa6c0 | ||
|
|
87b83465b3 | ||
|
|
a1900f0130 | ||
|
|
cf9ecff529 | ||
|
|
db3dee81e9 | ||
|
|
798032be08 | ||
|
|
d6834dca06 | ||
|
|
933cd7229a | ||
|
|
8e51a86971 | ||
|
|
8ab44041eb | ||
|
|
79191d757a | ||
|
|
7d01f54d73 | ||
|
|
63c0e11cc9 | ||
|
|
7347d03f93 | ||
|
|
fffb10d7e6 | ||
|
|
91b3de1444 | ||
|
|
8340f2f78a | ||
|
|
e65840d269 | ||
|
|
c245bc24a6 | ||
|
|
71ebc3dc8a | ||
|
|
7ade321dd3 | ||
|
|
19c0114eea | ||
|
|
968c0cf8fc | ||
|
|
f8d4874f2f | ||
|
|
261d6293dd | ||
|
|
988265fd9f | ||
|
|
ba03894e94 | ||
|
|
556c28d4b9 | ||
|
|
9d2a8c6b69 | ||
|
|
89b517f47e | ||
|
|
150016f543 | ||
|
|
5df599be15 | ||
|
|
0fd4b4b274 | ||
|
|
dd744053f4 | ||
|
|
be68c0d714 | ||
|
|
2a4b0d3f4b | ||
|
|
46dd002e5d | ||
|
|
04bc0ace3f | ||
|
|
fa6cc49343 | ||
|
|
231d191556 | ||
|
|
b6c8141042 | ||
|
|
ae209a37f7 | ||
|
|
0d07ae1b4c | ||
|
|
cfeed4f432 | ||
|
|
6ca243c05c | ||
|
|
cfa2f0f1bc | ||
|
|
d4456555b3 | ||
|
|
01babca0f4 | ||
|
|
123444ddf6 | ||
|
|
aeeb894ad9 | ||
|
|
d290fe4096 | ||
|
|
565e3e1b9d | ||
|
|
b23fe8319b | ||
|
|
eb41b4285e | ||
|
|
ab684f64ff | ||
|
|
abea28ef00 | ||
|
|
8934645c4d | ||
|
|
68d64665a6 | ||
|
|
5099d0e67e | ||
|
|
7a3ba3ae03 | ||
|
|
80272b1e27 | ||
|
|
8d807289ae | ||
|
|
cb946209a2 | ||
|
|
890b58d0ce | ||
|
|
6623f2559d | ||
|
|
b395eba4f3 | ||
|
|
c13f3d5b77 | ||
|
|
13c7373917 | ||
|
|
b0cb3dda37 | ||
|
|
a0ab6700d8 | ||
|
|
ebf4cb53b5 | ||
|
|
9fa2e967dc | ||
|
|
f853b410e7 | ||
|
|
874f717bd1 | ||
|
|
479a41c900 | ||
|
|
a0a1e2837c | ||
|
|
bd783385de | ||
|
|
5f19b3e47b | ||
|
|
c7fe0a5b5c | ||
|
|
5eb1b663f4 | ||
|
|
1e194385b8 | ||
|
|
731fb87012 | ||
|
|
5e1634dbfa | ||
|
|
7d366386da | ||
|
|
b4a03778d3 | ||
|
|
657924532e | ||
|
|
9099909983 | ||
|
|
9cf94ceb86 | ||
|
|
c529f474fe | ||
|
|
e31a5979bb | ||
|
|
90ee45349c | ||
|
|
ab7305b7b2 | ||
|
|
af0cdb006a | ||
|
|
326906f073 | ||
|
|
d54a6cbb71 | ||
|
|
94dab01e6b | ||
|
|
afa7b2259f | ||
|
|
dad1aec874 | ||
|
|
14c7322cde | ||
|
|
7b07146afa | ||
|
|
296c21fd3f | ||
|
|
5215d0f298 | ||
|
|
daa27e4989 | ||
|
|
69b7be744a | ||
|
|
7fa1353173 | ||
|
|
3653e9d63d | ||
|
|
0a2e087eaa | ||
|
|
11b683a156 | ||
|
|
d387a40154 | ||
|
|
0023be40c1 | ||
|
|
529db79ea4 | ||
|
|
32a27f2d98 | ||
|
|
09f2e29f27 | ||
|
|
fa4a1a4e8c | ||
|
|
8206a49dc7 | ||
|
|
6ad66f0d14 | ||
|
|
6399f43677 | ||
|
|
3a5c85e6dd | ||
|
|
ac440d814e | ||
|
|
270f2f8de4 | ||
|
|
8ad03fd30b | ||
|
|
10837e3841 | ||
|
|
9b33b27463 | ||
|
|
e0f6384dde | ||
|
|
6cbfe1440a | ||
|
|
b68b639d57 | ||
|
|
83dd8b0a24 | ||
|
|
96730f3879 | ||
|
|
50e5e8c82d | ||
|
|
38df3a8f9d | ||
|
|
b3f95b9231 |
@@ -0,0 +1,5 @@
|
||||
[run]
|
||||
omit =
|
||||
helpers/*
|
||||
tests/*
|
||||
setup.py
|
||||
@@ -0,0 +1,9 @@
|
||||
## Description:
|
||||
|
||||
|
||||
**Related issue (if applicable):** fixes #<blinkpy issue number goes here>
|
||||
|
||||
## 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
|
||||
+3
-2
@@ -1,3 +1,4 @@
|
||||
.pytest_cache/*
|
||||
.cache/*
|
||||
.tox/*
|
||||
__pycache__/*
|
||||
@@ -6,6 +7,6 @@ htmlcov/*
|
||||
*.pyc
|
||||
*.egg*/*
|
||||
dist/*
|
||||
MANIFEST
|
||||
README
|
||||
.sh
|
||||
build/*
|
||||
docs/_build
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
image: python
|
||||
|
||||
stages:
|
||||
- test
|
||||
|
||||
before_script:
|
||||
- curl -O https://bootstrap.pypa.io/get-pip.py
|
||||
- python get-pip.py
|
||||
- pip install tox
|
||||
|
||||
python35:
|
||||
image: python:3.5
|
||||
stage: test
|
||||
script: tox -e py35
|
||||
|
||||
python36:
|
||||
image: python:3.6
|
||||
stage: test
|
||||
script: tox -e py36
|
||||
|
||||
lint:
|
||||
image: python:3.6
|
||||
stage: test
|
||||
script: tox -e lint
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
python:
|
||||
enabled: true
|
||||
ignore_file: setup.py
|
||||
ignored_directories:
|
||||
- docs
|
||||
|
||||
fail_on_violations: true
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
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
|
||||
@@ -0,0 +1,287 @@
|
||||
# BlinkMonitorProtocol
|
||||
Unofficial documentation for the Client API of the Blink Wire-Free HD Home Monitoring and Alert System.
|
||||
|
||||
Copied from https://github.com/MattTW/BlinkMonitorProtocol
|
||||
|
||||
I am not affiliated with the company in any way - this documentation is strictly **"AS-IS"**. My goal was to uncover enough to arm and disarm the system programatically so that I can issue those commands in sync with my home alarm system arm/disarm. Just some raw notes at this point but should be enough for creating programmatic APIs. Lots more to be discovered and documented - feel free to contribute!
|
||||
|
||||
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.
|
||||
|
||||
**Request:**
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "Content-Type: application/json" --data-binary '{
|
||||
> "password" : "*your blink password*",
|
||||
> "client_specifier" : "iPhone 9.2 | 2.2 | 222",
|
||||
> "email" : "*your blink login/email*"
|
||||
>}' --compressed https://prod.immedia-semi.com/login
|
||||
|
||||
**Response:**
|
||||
>{"authtoken":{"authtoken":"*an auth token*","message":"auth"}}
|
||||
|
||||
**Notes:**
|
||||
The authtoken value is passed in a header in future calls.
|
||||
|
||||
##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
|
||||
|
||||
**Response:**
|
||||
JSON response containing information including Network ID and Account ID.
|
||||
|
||||
**Notes:**
|
||||
Network ID is needed to issue arm/disarm calls
|
||||
|
||||
|
||||
## Sync Modules
|
||||
|
||||
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
|
||||
|
||||
**Response:**
|
||||
JSON response containing information about the known state of the Sync module, most notably if it is online
|
||||
|
||||
**Notes:**
|
||||
Probably not strictly needed but checking result can verify that the sync module is online and will respond to requests to arm/disarm, etc.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
**Response:**
|
||||
JSON response containing information about the arm command request, including the command/request ID
|
||||
|
||||
**Notes:**
|
||||
When this call returns, it does not mean the arm request is complete, the client must gather the request ID from the response and poll for the status of the command.
|
||||
|
||||
## Disarm
|
||||
|
||||
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
|
||||
|
||||
**Response:**
|
||||
JSON response containing information about the disarm command request, including the command/request ID
|
||||
|
||||
**Notes:**
|
||||
When this call returns, it does not mean the disarm request is complete, the client must gather the request ID from the response and poll for the status of the command.
|
||||
|
||||
|
||||
## Command Status
|
||||
|
||||
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*
|
||||
|
||||
**Response:**
|
||||
JSON response containing state information of the given command, most notably whether it has completed and was successful.
|
||||
|
||||
**Notes:**
|
||||
After an arm/disarm command, the client appears to poll this URL every second or so until the response indicates the command is complete.
|
||||
|
||||
**Known Commands:**
|
||||
lv_relay, arm, disarm, thumbnail, clip
|
||||
|
||||
## Home Screen
|
||||
|
||||
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
|
||||
|
||||
**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.
|
||||
|
||||
**Notes:**
|
||||
Not necessary to as part of issuing arm/disarm commands, but contains good summary info.
|
||||
|
||||
##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*
|
||||
|
||||
**Response**
|
||||
A json list of evets incluing URL's. Replace the "mp4" with "jpg" extension to get the thumbnail of each clip
|
||||
|
||||
|
||||
**Request**
|
||||
Get a video clip from the events list
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed **video url from events list.mp4** > video.mp4
|
||||
|
||||
**Response**
|
||||
The mp4 video
|
||||
|
||||
**Request**
|
||||
Get a thumbnail from the events list
|
||||
|
||||
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed **video url from events list.jpg** > video_thumb.jpg
|
||||
|
||||
**Response**
|
||||
The jpg bytes.
|
||||
|
||||
**Notes**
|
||||
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
|
||||
|
||||
**Response**
|
||||
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
|
||||
|
||||
**Response**
|
||||
Command 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
|
||||
|
||||
**Response**
|
||||
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
|
||||
|
||||
**Response**
|
||||
JSON response containing a set of video information, including: camera name, creation time, thumbnail URI, size, length
|
||||
|
||||
**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*
|
||||
|
||||
**Response**
|
||||
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
|
||||
|
||||
**Response**
|
||||
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
|
||||
|
||||
**Response**
|
||||
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
|
||||
|
||||
**Response**
|
||||
Unknown - not tested
|
||||
|
||||
## Cameras
|
||||
|
||||
**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
|
||||
|
||||
**Response**
|
||||
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*
|
||||
|
||||
**Response**
|
||||
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
|
||||
|
||||
**Response**
|
||||
JSON response containing camera sensor information, such as wifi strength, temperature, and battery level
|
||||
|
||||
**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
|
||||
|
||||
**Response**
|
||||
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
|
||||
|
||||
**Response**
|
||||
JSON response containing camera information
|
||||
|
||||
*Note*: enabling or disabling motion detection is independent of arming or disarming the system. No motion detection or video recording will take place unless the system is armed.
|
||||
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
**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
|
||||
|
||||
**Response**
|
||||
JSON response containing client information, including: type, name, connection time, user ID
|
||||
|
||||
**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
|
||||
|
||||
**Response**
|
||||
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
|
||||
|
||||
**Response**
|
||||
"all ports tested are open"
|
||||
|
||||
**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
|
||||
|
||||
**Response**
|
||||
Unknown.
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
Changelog
|
||||
-----------
|
||||
|
||||
A list of changes between each release
|
||||
|
||||
0.9.0.dev (Development Version)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
0.8.0 (2018-05-21)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Added support for battery voltage level (fixes `#64 <https://github.com/fronzbot/blinkpy/issues/64>`_)
|
||||
- Added motion detection per camera
|
||||
- Added fully accessible camera configuration dict
|
||||
- Added celcius property to camera (fixes `#60 <https://github.com/fronzbot/blinkpy/issues/60>`_)
|
||||
|
||||
0.7.1 (2018-05-09)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Fixed pip 10 import issue during setup (`@fronzbot <https://github.com/fronzbot/blinkpy/pull/61>`_)
|
||||
|
||||
0.7.0 (2018-02-08)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- Fixed style errors for bumped pydocstring and pylint versions
|
||||
- Changed Blink.cameras dictionary to be case-insensitive (fixes `#35 <https://github.com/fronzbot/blinkpy/issues/35>`_)
|
||||
- Changed api endpoint for video extraction (fixes `#35 <https://github.com/fronzbot/blinkpy/issues/35>`_ and `#41 <https://github.com/fronzbot/blinkpy/issues/41>`_)
|
||||
- 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>`_)
|
||||
- Added authentication to ``login()`` function and deprecated ``setup_system()`` in favor of ``start()``
|
||||
- Added ``attributes`` dictionary to camera object
|
||||
|
||||
0.6.0 (2017-05-12)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
- Removed redundent properties that only called hidden variables
|
||||
- Revised request wrapper function to be more intelligent
|
||||
- Added tests to ensure exceptions are caught and handled (100% coverage!)
|
||||
- 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)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
- Fixed packaging mishap, same as 0.5.0 otherwise
|
||||
|
||||
0.5.0 (2017-03-12)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
- Fixed region handling problem
|
||||
- Added rest.piri subdomain as a backup if region can't be found
|
||||
- Improved the file writing function
|
||||
- Large test coverage increase
|
||||
|
||||
0.4.4 (2017-03-06)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
- Fixed bug where region id was not being set in the header
|
||||
|
||||
0.4.3 (2017-03-05)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
- Changed to bdist_wheel release
|
||||
|
||||
0.4.2 (2017-01-28)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
- Fixed inability to retrieve motion data due to Key Error
|
||||
|
||||
0.4.1 (2017-01-27)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
- Fixed refresh bug (0.3.1 did not actually fix the problem)
|
||||
- Image refresh routine added (per camera)
|
||||
- Dictionary of thumbnails per camera added
|
||||
- Improved test coverage
|
||||
|
||||
0.3.1 (2017-01-25)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
- Fixed refresh bug (Key Error)
|
||||
|
||||
0.3.0 (2017-01-25)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
- Added device id to camera lookup table
|
||||
- Added image to file method
|
||||
|
||||
0.2.0 (2017-01-21)
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
- Initial release of blinkpy
|
||||
@@ -0,0 +1,83 @@
|
||||
# Contributing to blinkpy
|
||||
|
||||
Everyone is welcome to contribute to blinkpy! The process to get started is described below.
|
||||
|
||||
## Fork the Repository
|
||||
|
||||
You can do this right in gituhb: just click the 'fork' button at the top right.
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
Now you can make changes to your code. It is worthwhile to test your code as you progress (see the **Testing** section)
|
||||
|
||||
## 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``
|
||||
|
||||
```shell
|
||||
$ pip3 install tox
|
||||
```
|
||||
|
||||
You can then run all of the tests with the following command:
|
||||
|
||||
```shell
|
||||
$ tox
|
||||
```
|
||||
|
||||
### Tips
|
||||
|
||||
If you only want to see if you can pass the local tests, you can run ``tox -e 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 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)
|
||||
|
||||
# Catching Up With Reality
|
||||
|
||||
If your code is taking a while to develop, you may be behind the ``dev`` branch, in which case you need to catch up before creating your pull-request. To do this you can run ``git rebase`` as follows (running this on your local branch):
|
||||
|
||||
```shell
|
||||
$ git fetch upstream dev
|
||||
$ git rebase upstream/dev
|
||||
```
|
||||
|
||||
If rebase detects conflicts, repeat the following process until all changes have been resolved:
|
||||
|
||||
1. ``git status`` shows you the filw with a conflict. You will need to edit that file and resolve the lines between ``<<<< | >>>>`.
|
||||
2. Add the modified file: ``git add <file>`` or ``git add .``.
|
||||
3. Continue rebase: ``git rebase --continue``.
|
||||
4. Repeat until all conflicts resolved.
|
||||
|
||||
# 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.
|
||||
@@ -0,0 +1,4 @@
|
||||
include README.rst
|
||||
include LICENSE.md
|
||||
include API.md
|
||||
include tests/*.py
|
||||
+61
-97
@@ -1,9 +1,10 @@
|
||||
**blinkpy**
|
||||
============
|
||||
blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version|
|
||||
================================================================
|
||||
A Python library for the Blink Camera system
|
||||
Only compatible with Python 3+
|
||||
|
||||
**Disclaimers**
|
||||
===============
|
||||
Disclaimer:
|
||||
~~~~~~~~~~~~~~~
|
||||
Published under the MIT license - See LICENSE file for more details.
|
||||
|
||||
"Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark owned by Immedia Inc., see www.blinkforhome.com for more information.
|
||||
@@ -11,118 +12,81 @@ I am in no way affiliated with Blink, nor Immedia Inc.
|
||||
|
||||
Original protocol hacking by MattTW : https://github.com/MattTW/BlinkMonitorProtocol
|
||||
|
||||
**Installation**
|
||||
API calls faster than 60 seconds is not recommended as it can overwhelm Blink's servers. Please use this module responsibly.
|
||||
|
||||
Installation
|
||||
================
|
||||
``pip3 install blinkpy``
|
||||
|
||||
**Purpose**
|
||||
Installing Development Version
|
||||
==================================
|
||||
To install the current development version, perform the following steps. Note that the following will create a blinkpy directory in your home area:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ cd ~
|
||||
$ git clone https://github.com/fronzbot/blinkpy.git
|
||||
$ cd blinkpy
|
||||
$ rm -rf build dist
|
||||
$ python3 setup.py bdist_wheel
|
||||
$ pip3 install --upgrade dist/*.whl
|
||||
|
||||
|
||||
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
|
||||
|
||||
**Usage**
|
||||
Usage
|
||||
=========
|
||||
In terms of usage, you just need to instantiate the module with a username and password
|
||||
::
|
||||
import blinkpy
|
||||
blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD')
|
||||
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
|
||||
|
||||
If you leave out either of those parameters, you need to call the login function which will prompt for your username and password
|
||||
::
|
||||
blink.login()
|
||||
from blinkpy import blinkpy
|
||||
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.
|
||||
|
||||
Once the login information is entered, you can run the `setup_system()` function which will attempt to authenticate with Blink servers using your username and password, obtain network ids, and create a list of cameras.
|
||||
The cameras are of a BlinkCamera class, of which the following parameters can be used (the code below creates a Blink object and iterates through each camera found)
|
||||
::
|
||||
blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD')
|
||||
blink.setup_system()
|
||||
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.
|
||||
|
||||
for camera in blink.cameras:
|
||||
print(camera.name) # Name of the camera
|
||||
print(camera.id) # Integer id of the camera (assigned by Blink)
|
||||
print(camera.armed) # Whether the device is armed/disarmed (ie. detecting motion)
|
||||
print(camera.clip) # Link to last motion clip captured
|
||||
print(camera.thumbnail) # Link to current camera thumbnail
|
||||
print(camera.temperature) # Current camera temperature (not super accurate, but might be useful for someone)
|
||||
print(camera.battery) # Current battery level... I think the value ranges from 0-3, but not quite sure yet.
|
||||
print(camera.notifications) # Number of unread notifications (ie. motion alerts that haven't been viewed)
|
||||
print(camera.motion) # Dictionary containing values for keys ['video', 'image', 'time']
|
||||
# which correspond to last motion recorded, thumbnail of last motion, and timestamp of last motion
|
||||
The below code will display cameras and their available attributes:
|
||||
|
||||
.. code:: python
|
||||
|
||||
**class Blink**
|
||||
---------------
|
||||
The following properties/methods are availiable to the Blink class
|
||||
|
||||
**Blink.cameras**
|
||||
Returns a list of BlinkCamera objects found by the system
|
||||
|
||||
**Blink.network_id**
|
||||
Returns the current network id
|
||||
|
||||
**Blink.account_id**
|
||||
Returns the account id
|
||||
|
||||
**Blink.events**
|
||||
Returns a list of events recorded by blink. This information will contain links to any motion caught by an armed camera.
|
||||
|
||||
**Blink.online**
|
||||
Returns online status of sync module (True = online, False = offline)
|
||||
|
||||
**Blink.last_motion()**
|
||||
Finds last motion information for each camera and stores it in the BlinkCamera.motion field
|
||||
|
||||
**Blink.arm**
|
||||
Set to True to arm, False to disarm. Can be used to see the status of the system as well
|
||||
|
||||
**Blink.refresh()**
|
||||
Forces a refresh of all camera information
|
||||
|
||||
**Blink.get_summary()**
|
||||
Returns json formatted summary of the system
|
||||
|
||||
**Blink.get_cameras()**
|
||||
Finds all cameras in the system and creates them
|
||||
|
||||
**Blink.set_links()**
|
||||
Gives each BlinkCamera object the links needed to find recent images and videos
|
||||
|
||||
**Blink.login()**
|
||||
Prompts user for login information
|
||||
|
||||
**Blink.get_auth_token()**
|
||||
Uses login information to retrieve authorization token from Blink for further communication
|
||||
|
||||
**Blink.get_ids()**
|
||||
Retrieves the network_id and account_id from Blink in order to access video and image pages on their server
|
||||
|
||||
**Blink.setup_system()**
|
||||
A wrapper script that calls:
|
||||
::
|
||||
self.get_auth_token()
|
||||
self.get_ids()
|
||||
self.get_camers()
|
||||
self.set_links()
|
||||
|
||||
|
||||
**class BlinkCamera**
|
||||
---------------------
|
||||
This class is just a wrapper for each individual camera in order to make communication with individual cameras less clunky. The following properties/methods are availiable (in addition to the ones mentioned earlier)
|
||||
|
||||
**BlinkCamera.snap_picture()**
|
||||
Takes an image with the camera and saves it as the new thumbnail. The Blink.refresh() method should be called after this if you want to store the new thumbnail link
|
||||
|
||||
**BlinkCamera.set_motion_detect(enable=True/False)**
|
||||
Sending True to this function will enable motion detection for the camera. Setting to False will disable motion detection
|
||||
|
||||
**BlinkCamera.image_to_file(path)**
|
||||
This will write the current thumbnail to the location indicated in 'path'
|
||||
from blinkpy import blinkpy
|
||||
|
||||
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
|
||||
|
||||
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']
|
||||
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)
|
||||
|
||||
The ``blinkpy`` api also allows for saving images and videos to a file and snapping a new picture from the camera remotely:
|
||||
|
||||
.. code:: python
|
||||
|
||||
camera = blink.sync.camera['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')
|
||||
|
||||
.. |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
|
||||
.. |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
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from blinkpy import Blink
|
||||
-396
@@ -1,396 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
blinkpy by Kevin Fronczak - A Blink camera Python library
|
||||
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.
|
||||
'''
|
||||
|
||||
import logging
|
||||
import requests
|
||||
import getpass
|
||||
import json
|
||||
|
||||
__version__ = '0.4.1'
|
||||
|
||||
BLINK_URL = 'immedia-semi.com'
|
||||
LOGIN_URL = 'https://prod.' + BLINK_URL + '/login'
|
||||
BASE_URL = 'https://prod.' + BLINK_URL
|
||||
DEFAULT_URL = 'prod.' + BLINK_URL
|
||||
|
||||
logger = logging.getLogger('blinkpy')
|
||||
|
||||
|
||||
def _request(url, data=None, headers=None, type='get', stream=False, json=True):
|
||||
"""Wrapper function for request"""
|
||||
if type is 'post':
|
||||
response = requests.post(url, headers=headers, data=data).json()
|
||||
elif type is 'get' and json:
|
||||
response = requests.get(url, headers=headers, stream=stream).json()
|
||||
elif type is 'get' and not json:
|
||||
response = requests.get(url, headers=headers, stream=stream)
|
||||
else:
|
||||
raise ValueError("Cannot perform requests of type " + type)
|
||||
|
||||
if json and 'message' in response.keys():
|
||||
raise BlinkAuthenticationException(response['code'], response['message'])
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class BlinkException(Exception):
|
||||
def __init__(self, id, message):
|
||||
self.id = id
|
||||
self.message = message
|
||||
|
||||
|
||||
class BlinkAuthenticationException(BlinkException):
|
||||
pass
|
||||
|
||||
|
||||
class BlinkCamera(object):
|
||||
"""Class to initialize individual camera"""
|
||||
def __init__(self, config):
|
||||
self._ID = str(config['device_id'])
|
||||
self._NAME = config['name']
|
||||
self._STATUS = config['armed']
|
||||
self._THUMB = BASE_URL + config['thumbnail'] + '.jpg'
|
||||
self._CLIP = BASE_URL + config['thumbnail'] + '.mp4'
|
||||
self._TEMPERATURE = config['temp']
|
||||
self._BATTERY = config['battery']
|
||||
self._NOTIFICATIONS = config['notifications']
|
||||
self._MOTION = {}
|
||||
self._HEADER = None
|
||||
self._IMAGE_LINK = None
|
||||
self._ARM_LINK = None
|
||||
self._REGION_ID = config['region_id']
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._ID
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._NAME
|
||||
|
||||
@name.setter
|
||||
def name(self, value):
|
||||
self._NAME = value
|
||||
|
||||
@property
|
||||
def region_id(self):
|
||||
return self._REGION_ID
|
||||
|
||||
@property
|
||||
def armed(self):
|
||||
return self._STATUS
|
||||
|
||||
@property
|
||||
def clip(self):
|
||||
return self._CLIP
|
||||
|
||||
@clip.setter
|
||||
def clip(self, value):
|
||||
self._CLIP = value
|
||||
|
||||
@property
|
||||
def thumbnail(self):
|
||||
# RUN THUMB ACQ HERE
|
||||
return self._THUMB
|
||||
|
||||
@thumbnail.setter
|
||||
def thumbnail(self, value):
|
||||
self._THUMB = value
|
||||
|
||||
@property
|
||||
def temperature(self):
|
||||
return self._TEMPERATURE
|
||||
|
||||
@temperature.setter
|
||||
def temperature(self, value):
|
||||
self._TEMPERATURE = value
|
||||
|
||||
@property
|
||||
def battery(self):
|
||||
return self._BATTERY
|
||||
|
||||
@battery.setter
|
||||
def battery(self, value):
|
||||
self._BATTERY = value
|
||||
|
||||
@property
|
||||
def notifications(self):
|
||||
return self._NOTIFICATIONS
|
||||
|
||||
@notifications.setter
|
||||
def notifications(self, value):
|
||||
self._NOTIFICATIONS = value
|
||||
|
||||
@property
|
||||
def image_link(self):
|
||||
return self._IMAGE_LINK
|
||||
|
||||
@image_link.setter
|
||||
def image_link(self, value):
|
||||
self._IMAGE_LINK = value
|
||||
|
||||
@property
|
||||
def arm_link(self):
|
||||
return self._ARM_LINK
|
||||
|
||||
@arm_link.setter
|
||||
def arm_link(self, value):
|
||||
self._ARM_LINK = value
|
||||
|
||||
@property
|
||||
def header(self):
|
||||
return self._HEADER
|
||||
|
||||
@header.setter
|
||||
def header(self, value):
|
||||
self._HEADER = value
|
||||
|
||||
@property
|
||||
def motion(self):
|
||||
return self._MOTION
|
||||
|
||||
@motion.setter
|
||||
def motion(self, value):
|
||||
"""Sets link to last motion and timestamp"""
|
||||
self._MOTION = value
|
||||
|
||||
def snap_picture(self):
|
||||
"""Takes a picture with camera to create a new thumbnail"""
|
||||
_request(self._IMAGE_LINK, headers=self._HEADER, type='post')
|
||||
|
||||
def set_motion_detect(self, enable):
|
||||
"""Sets motion detection"""
|
||||
url = self._ARM_LINK
|
||||
if enable:
|
||||
_request(url + 'enable', headers=self._HEADER, type='post')
|
||||
else:
|
||||
_request(url + 'disable', headers=self._HEADER, type='post')
|
||||
|
||||
def update(self, values):
|
||||
self._NAME = values['name']
|
||||
self._STATUS = values['armed']
|
||||
self._THUMB = BASE_URL + values['thumbnail'] + '.jpg'
|
||||
self._CLIP = BASE_URL + values['thumbnail'] + '.mp4'
|
||||
self._TEMPERATURE = values['temp']
|
||||
self._BATTERY = values['battery']
|
||||
self._NOTIFICATIONS = values['notifications']
|
||||
|
||||
def image_refresh(self):
|
||||
url = BASE_URL + '/homescreen'
|
||||
response = _request(url, headers=self._HEADER, type='get')['devices']
|
||||
for element in response:
|
||||
try:
|
||||
if str(element['device_id']) == self._ID:
|
||||
self._THUMB = BASE_URL + element['thumbnail'] + '.jpg'
|
||||
return self._THUMB
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def image_to_file(self, path):
|
||||
thumb = self.image_refresh()
|
||||
response = _request(thumb, headers=self._HEADER, stream=True, json=False)
|
||||
if response.status_code == 200:
|
||||
with open(path, 'wb') as f:
|
||||
for chunk in response:
|
||||
f.write(chunk)
|
||||
|
||||
|
||||
class Blink(object):
|
||||
"""Class to initialize communication and sync module"""
|
||||
def __init__(self, username=None, password=None):
|
||||
"""Constructor for class"""
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._TOKEN = None
|
||||
self._AUTH_HEADER = None
|
||||
self._NETWORKID = None
|
||||
self._ACCOUNTID = None
|
||||
self._REGION = None
|
||||
self._REGION_ID = None
|
||||
self._HOST = None
|
||||
self._EVENTS = []
|
||||
self._CAMERAS = {}
|
||||
self._IDLOOKUP = {}
|
||||
|
||||
@property
|
||||
def cameras(self):
|
||||
return self._CAMERAS
|
||||
|
||||
@property
|
||||
def camera_thumbs(self):
|
||||
self.refresh()
|
||||
data = {}
|
||||
for name, camera in self._CAMERAS.items():
|
||||
data[name] = camera.thumbnail
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def id_table(self):
|
||||
return self._IDLOOKUP
|
||||
|
||||
@property
|
||||
def network_id(self):
|
||||
return self._NETWORKID
|
||||
|
||||
@property
|
||||
def account_id(self):
|
||||
return self._ACCOUNTID
|
||||
|
||||
@property
|
||||
def region(self):
|
||||
return self._REGION
|
||||
|
||||
@property
|
||||
def region_id(self):
|
||||
return self._REGION_ID
|
||||
|
||||
@property
|
||||
def events(self):
|
||||
"""Gets all events on server"""
|
||||
url = BASE_URL + '/events/network/' + self._NETWORKID
|
||||
headers = self._AUTH_HEADER
|
||||
self._EVENTS = _request(url, headers=headers, type='get')['events']
|
||||
return self._EVENTS
|
||||
|
||||
@property
|
||||
def online(self):
|
||||
"""Returns True or False depending on if sync module is online/offline"""
|
||||
url = BASE_URL + 'network/' + self._NETWORKID + '/syncmodules'
|
||||
headers = self._AUTH_HEADER
|
||||
online_dict = {'online': True, 'offline': False}
|
||||
return online_dict[_request(url, headers=headers, type='get')['syncmodule']['status']]
|
||||
|
||||
def last_motion(self):
|
||||
"""Finds last motion of each camera"""
|
||||
recent = self.events
|
||||
for element in recent:
|
||||
try:
|
||||
camera_id = str(element['camera_id'])
|
||||
camera_name = self._IDLOOKUP[camera_id]
|
||||
camera = self._CAMERAS[camera_name]
|
||||
if element['type'] == 'motion':
|
||||
url = BASE_URL + element['video_url']
|
||||
camera.motion = {'video': url, 'image': url[:-3] + 'jpg', 'time': element['created_at']}
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def arm(self):
|
||||
"""Returns status of sync module: armed/disarmed"""
|
||||
return self.get_summary()['network']['armed']
|
||||
|
||||
@arm.setter
|
||||
def arm(self, value):
|
||||
"""Arms or disarms system. Arms/disarms all if camera not named"""
|
||||
if value:
|
||||
value_to_append = 'arm'
|
||||
else:
|
||||
value_to_append = 'disarm'
|
||||
url = BASE_URL + '/network/' + self._NETWORKID + '/' + value_to_append
|
||||
_request(url, headers=self._AUTH_HEADER, type='post')
|
||||
|
||||
def refresh(self):
|
||||
"""Gets all blink cameras and pulls their most recent status"""
|
||||
response = self.get_summary()['devices']
|
||||
|
||||
for name, camera in self._CAMERAS.items():
|
||||
for element in response:
|
||||
try:
|
||||
if str(element['device_id']) == camera.id:
|
||||
camera.update(element)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def get_summary(self):
|
||||
"""Gets a full summary of device information"""
|
||||
url = BASE_URL + '/homescreen'
|
||||
headers = self._AUTH_HEADER
|
||||
|
||||
if self._AUTH_HEADER is None:
|
||||
raise BlinkException(0, "Authentication header incorrect. Are you sure you logged in and received your token?")
|
||||
|
||||
return _request(url, headers=headers, type='get')
|
||||
|
||||
def get_cameras(self):
|
||||
"""Finds and creates cameras"""
|
||||
response = self.get_summary()['devices']
|
||||
for element in response:
|
||||
if 'device_type' in element.keys():
|
||||
if element['device_type'] == 'camera':
|
||||
# Add region to config
|
||||
element['region_id'] = self._REGION_ID
|
||||
device = BlinkCamera(element)
|
||||
self._CAMERAS[device.name] = device
|
||||
self._IDLOOKUP[device.id] = device.name
|
||||
|
||||
def set_links(self):
|
||||
"""Sets access links and required headers for each camera in system"""
|
||||
for name, camera in self._CAMERAS.items():
|
||||
image_url = BASE_URL + '/network/' + self._NETWORKID + '/camera/' + camera.id + '/thumbnail'
|
||||
arm_url = BASE_URL + '/network/' + self._NETWORKID + '/camera/' + camera.id + '/'
|
||||
camera.image_link = image_url
|
||||
camera.arm_link = arm_url
|
||||
camera.header = self._AUTH_HEADER
|
||||
|
||||
def setup_system(self):
|
||||
"""Method logs in and sets auth token and network ids for future requests"""
|
||||
if self._username is None or self._password is None:
|
||||
raise BlinkAuthenticationException(3, "Cannot authenticate since either password or username has not been set")
|
||||
|
||||
self.get_auth_token()
|
||||
self.get_ids()
|
||||
self.get_cameras()
|
||||
self.set_links()
|
||||
|
||||
def login(self):
|
||||
"""Prompts user for username and password"""
|
||||
self._username = input("Username:")
|
||||
self._password = getpass.getpass("Password:")
|
||||
|
||||
def get_auth_token(self):
|
||||
"""Retrieves the authentication token from Blink"""
|
||||
if not isinstance(self._username, str):
|
||||
raise BlinkAuthenticationException(0, "Username must be a string")
|
||||
if not isinstance(self._password, str):
|
||||
raise BlinkAuthenticationException(0, "Password must be a string")
|
||||
|
||||
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"
|
||||
})
|
||||
response = _request(LOGIN_URL, headers=headers, data=data, type='post')
|
||||
self._TOKEN = response['authtoken']['authtoken']
|
||||
(self._REGION_ID, self._REGION), = response['region'].items()
|
||||
self._HOST = self._REGION + '.' + BLINK_URL
|
||||
self._AUTH_HEADER = {'Host': self._HOST,
|
||||
'TOKEN_AUTH': self._TOKEN
|
||||
}
|
||||
|
||||
def get_ids(self):
|
||||
"""Sets the network ID and Account ID"""
|
||||
url = BASE_URL + '/networks'
|
||||
headers = self._AUTH_HEADER
|
||||
|
||||
if self._AUTH_HEADER is None:
|
||||
raise BlinkException(0, "Authentication header incorrect. Are you sure you logged in and received your token?")
|
||||
|
||||
response = _request(url, headers=headers, type='get')
|
||||
self._NETWORKID = str(response['networks'][0]['id'])
|
||||
self._ACCOUNTID = str(response['networks'][0]['account_id'])
|
||||
@@ -0,0 +1 @@
|
||||
"""Init file for blinkpy."""
|
||||
@@ -0,0 +1,197 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
blinkpy by Kevin Fronczak - A Blink camera Python library.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
import getpass
|
||||
import logging
|
||||
import blinkpy.helpers.errors as ERROR
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.helpers.util import (
|
||||
http_req, create_session, BlinkURLHandler,
|
||||
BlinkException, BlinkAuthenticationException)
|
||||
from blinkpy.helpers.constants import (
|
||||
DEFAULT_URL, BLINK_URL, LOGIN_URL, LOGIN_BACKUP_URL)
|
||||
|
||||
REFRESH_RATE = 30
|
||||
|
||||
_LOGGER = logging.getLogger('blinkpy')
|
||||
|
||||
|
||||
class Blink():
|
||||
"""Class to initialize communication."""
|
||||
|
||||
def __init__(self, username=None, password=None,
|
||||
refresh_rate=REFRESH_RATE):
|
||||
"""
|
||||
Initialize Blink system.
|
||||
|
||||
:param username: Blink username (usually email address)
|
||||
:param password: Blink password
|
||||
:param refresh_rate: Refresh rate of blink information.
|
||||
Defaults to 15 (seconds)
|
||||
"""
|
||||
self._username = username
|
||||
self._password = password
|
||||
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.urls = None
|
||||
self.sync = None
|
||||
self.region = None
|
||||
self.region_id = None
|
||||
self.last_refresh = None
|
||||
self.refresh_rate = refresh_rate
|
||||
self.session = None
|
||||
self._login_url = LOGIN_URL
|
||||
|
||||
@property
|
||||
def events(self):
|
||||
"""Get all events on server."""
|
||||
return self._events
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Perform full system setup.
|
||||
|
||||
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:
|
||||
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()
|
||||
|
||||
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
|
||||
|
||||
def get_auth_token(self):
|
||||
"""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)
|
||||
|
||||
return self._auth_header
|
||||
|
||||
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'])
|
||||
|
||||
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')
|
||||
|
||||
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 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
|
||||
|
||||
def refresh(self, force_cache=False):
|
||||
"""
|
||||
Perform a system refresh.
|
||||
|
||||
: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)
|
||||
|
||||
def check_if_ok_to_update(self):
|
||||
"""Check if it is ok to perform an http request."""
|
||||
current_time = int(time.time())
|
||||
last_refresh = self.last_refresh
|
||||
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
|
||||
@@ -0,0 +1,218 @@
|
||||
"""Defines Blink cameras."""
|
||||
|
||||
from shutil import copyfileobj
|
||||
import logging
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MAX_CLIPS = 5
|
||||
|
||||
|
||||
class BlinkCamera():
|
||||
"""Class to initialize individual camera."""
|
||||
|
||||
def __init__(self, config, 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.motion_detected = None
|
||||
self.wifi_strength = None
|
||||
self.camera_config = dict()
|
||||
self.motion_enabled = None
|
||||
self.last_record = list()
|
||||
self._cached_image = None
|
||||
self._cached_video = None
|
||||
|
||||
@property
|
||||
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
|
||||
}
|
||||
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
|
||||
|
||||
@property
|
||||
def temperature_c(self):
|
||||
"""Return temperature in celcius."""
|
||||
return round((self.temperature - 32) / 9.0 * 5.0, 1)
|
||||
|
||||
@property
|
||||
def image_from_cache(self):
|
||||
"""Return the most recently cached image."""
|
||||
if self._cached_image:
|
||||
return self._cached_image
|
||||
return None
|
||||
|
||||
@property
|
||||
def video_from_cache(self):
|
||||
"""Return the most recently cached video."""
|
||||
if self._cached_video:
|
||||
return self._cached_video
|
||||
return None
|
||||
|
||||
def snap_picture(self):
|
||||
"""Take a picture with camera to create a new thumbnail."""
|
||||
self.sync.http_post(self.image_link)
|
||||
|
||||
def set_motion_detect(self, enable):
|
||||
"""Set motion detection."""
|
||||
url = self.arm_link
|
||||
if enable:
|
||||
self.sync.http_post("{}{}".format(url, 'enable'))
|
||||
else:
|
||||
self.sync.http_post("{}{}".format(url, 'disable'))
|
||||
|
||||
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']
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
def image_to_file(self, path):
|
||||
"""
|
||||
Write image to file.
|
||||
|
||||
:param path: Path to write file
|
||||
"""
|
||||
_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:
|
||||
copyfileobj(response.raw, imgfile)
|
||||
else:
|
||||
_LOGGER.error("Cannot write image to file, response %s",
|
||||
response.status_code)
|
||||
|
||||
def video_to_file(self, path):
|
||||
"""Write video to file.
|
||||
|
||||
:param path: Path to write file
|
||||
"""
|
||||
_LOGGER.debug("Writing video from %s to %s", self.name, path)
|
||||
response = self._cached_video
|
||||
with open(path, 'wb') as vidfile:
|
||||
copyfileobj(response.raw, vidfile)
|
||||
@@ -0,0 +1 @@
|
||||
"""Init file for blinkpy helper functions."""
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Generates constants for use in blinkpy."""
|
||||
|
||||
import os
|
||||
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 9
|
||||
PATCH_VERSION = 0
|
||||
|
||||
__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
|
||||
|
||||
REQUIRED_PYTHON_VER = (3, 4, 2)
|
||||
|
||||
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'
|
||||
]
|
||||
|
||||
PROJECT_GITHUB_USERNAME = 'fronzbot'
|
||||
PROJECT_GITHUB_REPOSITORY = 'blinkpy'
|
||||
|
||||
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
|
||||
|
||||
'''
|
||||
Dictionaries
|
||||
'''
|
||||
ONLINE = {'online': True, 'offline': False}
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Module to define error types."""
|
||||
|
||||
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"
|
||||
)
|
||||
AUTH_TOKEN = (
|
||||
3,
|
||||
"Authentication header incorrect. Are you sure you received your token?"
|
||||
)
|
||||
REQUEST = (4, "Cannot perform request (get/post type incorrect)")
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Useful functions for blinkpy."""
|
||||
|
||||
import logging
|
||||
from requests import Request, Session
|
||||
import blinkpy.helpers.errors as ERROR
|
||||
from blinkpy.helpers.constants import BLINK_URL
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_session():
|
||||
"""Create a session for blink communication."""
|
||||
sess = Session()
|
||||
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()
|
||||
return headers
|
||||
|
||||
|
||||
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.
|
||||
|
||||
:param blink: Blink instance
|
||||
:param url: URL to perform request
|
||||
:param data: Data to send (default: None)
|
||||
:param headers: Headers to send (default: None)
|
||||
:param reqtype: Can be 'get' or 'post' (default: 'get')
|
||||
:param stream: Stream response? True/FALSE
|
||||
:param json_resp: Return JSON response? TRUE/False
|
||||
:param is_retry: Is this a retry attempt? True/FALSE
|
||||
"""
|
||||
if reqtype == 'post':
|
||||
req = Request('POST', url, headers=headers, data=data)
|
||||
elif reqtype == 'get':
|
||||
req = Request('GET', url, headers=headers)
|
||||
else:
|
||||
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:
|
||||
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)
|
||||
|
||||
if json_resp:
|
||||
return response.json()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class BlinkException(Exception):
|
||||
"""Class to throw general blink exception."""
|
||||
|
||||
def __init__(self, errcode):
|
||||
"""Initialize BlinkException."""
|
||||
super().__init__()
|
||||
self.errid = errcode[0]
|
||||
self.message = errcode[1]
|
||||
|
||||
|
||||
class BlinkAuthenticationException(BlinkException):
|
||||
"""Class to throw authentication exception."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BlinkURLHandler():
|
||||
"""Class that handles Blink URLS."""
|
||||
|
||||
def __init__(self, region_id):
|
||||
"""Initialize the urls."""
|
||||
self.base_url = "https://rest.{}.{}".format(region_id, 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)
|
||||
@@ -0,0 +1,251 @@
|
||||
"""Defines a sync module for Blink."""
|
||||
|
||||
import logging
|
||||
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from blinkpy.camera import BlinkCamera
|
||||
from blinkpy.helpers.util import http_req
|
||||
from blinkpy.helpers.constants import ONLINE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BlinkSyncModule():
|
||||
"""Class to initialize sync module."""
|
||||
|
||||
def __init__(self, blink, header, urls=None):
|
||||
"""
|
||||
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.sync_id = None
|
||||
self.region = None
|
||||
self.region_id = None
|
||||
self._host = 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
|
||||
|
||||
@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
|
||||
|
||||
@property
|
||||
def arm(self):
|
||||
"""Return status of sync module: armed/disarmed."""
|
||||
return self._summary['network']['armed']
|
||||
|
||||
@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)
|
||||
|
||||
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
|
||||
|
||||
def get_videos(self, start_page=0, end_page=1):
|
||||
"""
|
||||
Retrieve last recorded videos per camera.
|
||||
|
||||
: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)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def _summary_request(self):
|
||||
"""Request a summary from blink."""
|
||||
return self.blink.summary_request()
|
||||
|
||||
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')
|
||||
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = blinkpy
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../README.rst
|
||||
@@ -0,0 +1,30 @@
|
||||
.. _core_module:
|
||||
|
||||
:mod:`blinkpy`
|
||||
----------------------
|
||||
|
||||
.. automodule:: blinkpy.blinkpy
|
||||
|
||||
.. automodule:: blinkpy.sync_module
|
||||
|
||||
.. automodule:: blinkpy.camera
|
||||
|
||||
.. 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
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# blinkpy documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sat Jan 20 22:20:16 2018.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
import sphinx_rtd_theme
|
||||
from blinkpy.helpers.constants import __version__
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.mathjax',
|
||||
'sphinx.ext.viewcode']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'blinkpy'
|
||||
copyright = '2018, Kevin Fronczak'
|
||||
author = 'Kevin Fronczak'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = __version__
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = __version__
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
#html_theme = 'alabaster'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# This is required for the alabaster theme
|
||||
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
|
||||
html_sidebars = {
|
||||
'**': [
|
||||
'relations.html', # needs 'show_related': True theme option to display
|
||||
'searchbox.html',
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'blinkpydoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'blinkpy.tex', 'blinkpy Documentation',
|
||||
'Kevin Fronczak', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'blinkpy', 'blinkpy Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'blinkpy', 'blinkpy Documentation',
|
||||
author, 'blinkpy', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
.. blinkpy documentation master file, created by
|
||||
sphinx-quickstart on Sat Jan 20 22:20:16 2018.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to blinkpy's documentation!
|
||||
===================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
:glob:
|
||||
|
||||
README
|
||||
api/*
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
@@ -0,0 +1,36 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
set SPHINXPROJ=blinkpy
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
:end
|
||||
popd
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
import blinkpy
|
||||
import time
|
||||
|
||||
blink = blinkpy.Blink()
|
||||
blink.login()
|
||||
blink.setup_system()
|
||||
|
||||
for camera in blink.cameras:
|
||||
print('Arming ' + camera.name)
|
||||
camera.set_motion_detect(True)
|
||||
time.sleep(5)
|
||||
blink.refresh()
|
||||
|
||||
print('Arming Blink')
|
||||
blink.arm = True
|
||||
time.sleep(5)
|
||||
print('Blink armed? ' + str(blink.arm))
|
||||
print('Disarming Blink')
|
||||
time.sleep(5)
|
||||
blink.arm = False
|
||||
print('Blink armed? ' + str(blink.arm))
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
import blinkpy
|
||||
|
||||
blink = blinkpy.Blink()
|
||||
blink.login()
|
||||
blink.setup_system()
|
||||
|
||||
for camera in blink.cameras:
|
||||
print(camera.name) # Name of the camera
|
||||
print(camera.id) # Integer id of the camera (assigned by Blink)
|
||||
print(camera.armed) # Whether the device is armed/disarmed (ie. detecting motion)
|
||||
print(camera.clip) # Link to last motion clip captured
|
||||
print(camera.thumbnail) # Link to current camera thumbnail
|
||||
print(camera.temperature) # Current camera temperature (not super accurate, but might be useful for someone)
|
||||
print(camera.battery) # Current battery level... I think the value ranges from 0-3, but not quite sure yet.
|
||||
print(camera.notifications) # Number of unread notifications (ie. motion alerts that haven't been viewed)
|
||||
print(camera.motion) # Dictionary containing values for keys ['video', 'image', 'time']
|
||||
# which correspond to last motion recorded, thumbnail of last motion, and timestamp of last motion
|
||||
@@ -0,0 +1,25 @@
|
||||
[MASTER]
|
||||
reports=no
|
||||
|
||||
# Reasons disabled:
|
||||
# locally-disabled - it spams too much
|
||||
# duplicate-code - it's annoying
|
||||
# unused-argument - generic callbacks and setup methods create a lot of warnings
|
||||
# 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
|
||||
|
||||
disable=
|
||||
locally-disabled,
|
||||
unused-argument,
|
||||
duplicate-code,
|
||||
too-many-arguments,
|
||||
too-many-branches,
|
||||
too-many-instance-attributes,
|
||||
too-many-locals,
|
||||
too-many-public-methods,
|
||||
too-many-return-statements,
|
||||
too-many-statements,
|
||||
too-many-lines,
|
||||
too-few-public-methods,
|
||||
no-else-return,
|
||||
+1
-4
@@ -1,5 +1,2 @@
|
||||
pytest>=2.9.2
|
||||
pytest-cov>=2.3.1
|
||||
pytest-timeout>=1.0.0
|
||||
requests>=2.12.4
|
||||
testtools
|
||||
testtools
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
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
|
||||
restructuredtext-lint>=1.0.1
|
||||
pygments>=2.2.0
|
||||
@@ -1,27 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from os.path import abspath, dirname, join
|
||||
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,
|
||||
)
|
||||
|
||||
from blinkpy import __version__
|
||||
from setuptools import setup
|
||||
this_dir = abspath(dirname(__file__))
|
||||
|
||||
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]
|
||||
|
||||
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__
|
||||
|
||||
setup(
|
||||
name = 'blinkpy',
|
||||
name = PROJECT_PACKAGE_NAME,
|
||||
version = __version__,
|
||||
description = 'A Blink camera Python library',
|
||||
long_description='A library that communicates with Blink cameras',
|
||||
author = 'Kevin Fronczak',
|
||||
author_email = "kfronczak@gmail.com",
|
||||
license='MIT',
|
||||
url = 'https://github.com/fronzbot/blinkpy',
|
||||
py_modules=['blinkpy'],
|
||||
install_requires=['requests'],
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Environment :: Plugins',
|
||||
'Environment :: Web Environment'
|
||||
]
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Init file for tests directory."""
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Simple mock responses definitions."""
|
||||
|
||||
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'}
|
||||
}
|
||||
|
||||
|
||||
class MockResponse:
|
||||
"""Class for mock request response."""
|
||||
|
||||
def __init__(self, json_data, status_code, raw_data=None):
|
||||
"""Initialize mock get response."""
|
||||
self.json_data = json_data
|
||||
self.status_code = status_code
|
||||
self.raw_data = raw_data
|
||||
|
||||
def json(self):
|
||||
"""Return json data from get_request."""
|
||||
return self.json_data
|
||||
|
||||
@property
|
||||
def raw(self):
|
||||
"""Return raw data from get request."""
|
||||
return self.raw_data
|
||||
|
||||
|
||||
def mocked_session_send(*args, **kwargs):
|
||||
"""Mock session."""
|
||||
prepped = args[0]
|
||||
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}
|
||||
status = 400
|
||||
elif url == 'use_bad_response':
|
||||
response = {'foo': 'bar'}
|
||||
status = 200
|
||||
elif url == 'reauth':
|
||||
response = {'message': 'REAUTH', 'code': 777}
|
||||
status = 777
|
||||
else:
|
||||
response = {'test': 'foo'}
|
||||
status = 200
|
||||
elif method == 'POST':
|
||||
if url in (const.LOGIN_URL, const.LOGIN_BACKUP_URL):
|
||||
response = LOGIN_RESPONSE
|
||||
status = 200
|
||||
elif url == 'http://wrong.url/' or url is None:
|
||||
response = {'message': 'Error', 'code': 404}
|
||||
status = 404
|
||||
else:
|
||||
response = {'message': 'foo', 'code': 200}
|
||||
status = 200
|
||||
|
||||
return MockResponse(response, status)
|
||||
|
||||
|
||||
class MockURLHandler(BlinkURLHandler):
|
||||
"""Mocks URL Handler in blinkpy module."""
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,139 @@
|
||||
"""Tests camera and system functions."""
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from requests import Request
|
||||
|
||||
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
|
||||
import tests.mock_responses as mresp
|
||||
|
||||
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
|
||||
|
||||
|
||||
@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)
|
||||
# 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.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')
|
||||
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')
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(self.blink._token, 'foobar123')
|
||||
@@ -1,100 +0,0 @@
|
||||
import blinkpy
|
||||
import requests
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from blinkpy import LOGIN_URL
|
||||
from blinkpy import BASE_URL
|
||||
import test_const as const
|
||||
|
||||
def mocked_requests_post(*args, **kwargs):
|
||||
class MockPostResponse:
|
||||
def __init__(self,json_data,status_code):
|
||||
self.json_data = json_data
|
||||
self.status_code = status_code
|
||||
def json(self):
|
||||
return self.json_data
|
||||
|
||||
if args[0] == LOGIN_URL:
|
||||
return MockPostResponse({"region":{const.REGION_ID: const.REGION}, "authtoken":{"authtoken":const.TOKEN}}, 200)
|
||||
elif args[0].split("/")[-1] == 'arm':
|
||||
return MockPostResponse({"armed":True}, 200)
|
||||
elif args[0].split("/")[-1] == 'disarm':
|
||||
return MockPostResponse({"armed":False}, 200)
|
||||
else:
|
||||
return MockPostResponse({}, 200)
|
||||
|
||||
return MockPostResponse({'message':'ERROR','code':404}, 404)
|
||||
|
||||
def mocked_requests_get(*args, **kwargs):
|
||||
class MockGetResponse:
|
||||
def __init__(self,json_data,status_code):
|
||||
self.json_data = json_data
|
||||
self.status_code = status_code
|
||||
def json(self):
|
||||
return self.json_data
|
||||
|
||||
if args[0] == BASE_URL + '/networks':
|
||||
return MockGetResponse({'networks':[{"id":const.NETWORK_ID,"account_id":const.ACCOUNT_ID},{"nothing":"nothing"}]}, 200)
|
||||
else:
|
||||
return MockGetResponse(const.response, 200)
|
||||
|
||||
|
||||
return MockGetResponse({'message':'ERROR','code':404}, 404)
|
||||
|
||||
class TestBlinkRequests(unittest.TestCase):
|
||||
@mock.patch('blinkpy.requests.post', side_effect=mocked_requests_post)
|
||||
@mock.patch('blinkpy.requests.get', side_effect=mocked_requests_get)
|
||||
def test_blink_setup(self, mock_get, mock_post):
|
||||
blink = blinkpy.Blink(username='user',password='password')
|
||||
blink.setup_system()
|
||||
|
||||
self.assertEqual(blink.network_id, str(const.NETWORK_ID))
|
||||
self.assertEqual(blink.account_id, str(const.ACCOUNT_ID))
|
||||
self.assertEqual(blink.region, const.REGION)
|
||||
self.assertEqual(blink.region_id, const.REGION_ID)
|
||||
self.assertEqual(blink.online, const.ISONLINE)
|
||||
self.assertEqual(blink.arm, const.ARMED)
|
||||
|
||||
@mock.patch('blinkpy.requests.post', side_effect=mocked_requests_post)
|
||||
@mock.patch('blinkpy.requests.get', side_effect=mocked_requests_get)
|
||||
def test_blink_camera_setup_and_motion(self, mock_get, mock_post):
|
||||
blink = blinkpy.Blink(username='user',password='password')
|
||||
blink.setup_system()
|
||||
blink.last_motion()
|
||||
for name, camera in blink.cameras.items():
|
||||
if camera.id == str(const.DEVICE_ID):
|
||||
self.assertEqual(name, const.CAMERA_NAME)
|
||||
self.assertEqual(camera.armed, const.ARMED)
|
||||
self.assertEqual(camera.motion['video'], BASE_URL + const.THUMB + '.mp4')
|
||||
elif camera.id == str(const.DEVICE_ID2):
|
||||
self.assertEqual(name, const.CAMERA_NAME2)
|
||||
self.assertEqual(camera.armed, const.ARMED2)
|
||||
self.assertEqual(len(camera.motion.keys()), 0)
|
||||
else:
|
||||
assert False is True
|
||||
|
||||
@mock.patch('blinkpy.requests.post', side_effect=mocked_requests_post)
|
||||
@mock.patch('blinkpy.requests.get', side_effect=mocked_requests_get)
|
||||
def test_blink_refresh(self, mock_get, mock_post):
|
||||
blink = blinkpy.Blink(username='user',password='password')
|
||||
blink.setup_system()
|
||||
const.response['devices'][0]['thumbnail'] = const.THUMB + const.THUMB2
|
||||
blink.refresh()
|
||||
for name, camera in blink.cameras.items():
|
||||
if camera.id == str(const.DEVICE_ID):
|
||||
self.assertEqual(camera.thumbnail, BASE_URL + const.THUMB + const.THUMB2 + '.jpg')
|
||||
elif camera.id == str(const.DEVICE_ID2):
|
||||
pass
|
||||
else:
|
||||
assert False is True
|
||||
|
||||
const.response['devices'][0]['thumbnail'] = 'new'
|
||||
blink.cameras[const.CAMERA_NAME].image_refresh()
|
||||
for name, camera in blink.cameras.items():
|
||||
if camera.id == str(const.DEVICE_ID):
|
||||
self.assertEqual(camera.thumbnail, BASE_URL + 'new' + '.jpg')
|
||||
elif camera.id == str(const.DEVICE_ID2):
|
||||
pass
|
||||
else:
|
||||
assert False is True
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
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,215 @@
|
||||
"""
|
||||
Test all camera attributes.
|
||||
|
||||
Tests the camera initialization and attributes of
|
||||
individual BlinkCamera instantiations once the
|
||||
Blink system is set up.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
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'
|
||||
|
||||
CAMERA_CFG = {
|
||||
'camera': [
|
||||
{
|
||||
'battery_voltage': 90,
|
||||
'motion_alert': True,
|
||||
'wifi_strength': -30,
|
||||
'temperature': 68
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@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'
|
||||
}
|
||||
|
||||
header = {
|
||||
'Host': 'abc.zxc',
|
||||
'TOKEN_AUTH': mresp.LOGIN_RESPONSE['authtoken']['authtoken']
|
||||
}
|
||||
self.blink.session = create_session()
|
||||
self.blink.urls = BlinkURLHandler('test')
|
||||
self.blink.network_id = '0000'
|
||||
self.sync = BlinkSyncModule(self.blink, header, self.blink.urls)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
self.sync.cameras = {
|
||||
'foobar': BlinkCamera(self.camera_config, self.sync)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -1,75 +0,0 @@
|
||||
import testtools
|
||||
import unittest
|
||||
import blinkpy
|
||||
from blinkpy import BASE_URL
|
||||
|
||||
class TestBlink(unittest.TestCase):
|
||||
def test_blink_instance(self):
|
||||
blink = blinkpy.Blink()
|
||||
|
||||
def test_blink_camera_attributes(self):
|
||||
config = {'name':'name', 'device_id': 1248, 'armed':True, 'thumbnail':'/test/url/image', 'temp':70, 'battery':3, 'notifications':0, 'region_id':'rid'}
|
||||
|
||||
camera = blinkpy.BlinkCamera(config)
|
||||
|
||||
# Check that values are properly stored and can be recalled
|
||||
self.assertEqual(camera.name, config['name'])
|
||||
self.assertEqual(camera.id, str(config['device_id']))
|
||||
self.assertEqual(camera.armed, config['armed'])
|
||||
self.assertEqual(camera.clip, BASE_URL + config['thumbnail'] + '.mp4')
|
||||
self.assertEqual(camera.thumbnail, BASE_URL + config['thumbnail'] + '.jpg')
|
||||
self.assertEqual(camera.temperature, config['temp'])
|
||||
self.assertEqual(camera.battery, config['battery'])
|
||||
self.assertEqual(camera.notifications, config['notifications'])
|
||||
self.assertEqual(camera.region_id, config['region_id'])
|
||||
|
||||
# Check that values can be changed with individual methods
|
||||
test_header = {'Header':'test','Test':'1234'}
|
||||
test_img = 'http://image-link.com'
|
||||
test_arm = 'http://arm-link.com'
|
||||
test_motion = {'video':'url','image':'url','time':'timestamp'}
|
||||
|
||||
camera.name = config['name']+'_new'
|
||||
camera.clip = BASE_URL + config['thumbnail'] + '.mp4_new'
|
||||
camera.thumbnail = BASE_URL + config['thumbnail'] + '.jpg_new'
|
||||
camera.temperature = config['temp'] + 10
|
||||
camera.battery = config['battery'] + 1
|
||||
camera.notifications = config['notifications'] + 1
|
||||
camera.image_link = test_img
|
||||
camera.arm_link = test_arm
|
||||
camera.header = test_header
|
||||
camera.motion = test_motion
|
||||
|
||||
# Check that values are properly stored and can be recalled
|
||||
self.assertEqual(camera.name , config['name']+'_new')
|
||||
self.assertEqual(camera.clip , BASE_URL + config['thumbnail'] + '.mp4_new')
|
||||
self.assertEqual(camera.thumbnail, BASE_URL + config['thumbnail'] + '.jpg_new')
|
||||
self.assertEqual(camera.temperature, config['temp'] + 10)
|
||||
self.assertEqual(camera.battery, config['battery'] + 1)
|
||||
self.assertEqual(camera.notifications, config['notifications'] + 1)
|
||||
self.assertEqual(camera.image_link, test_img)
|
||||
self.assertEqual(camera.arm_link, test_arm)
|
||||
self.assertEqual(camera.header, test_header)
|
||||
self.assertEqual(camera.motion, test_motion)
|
||||
|
||||
# Verify bulk update function
|
||||
test_name = camera.name +' last'
|
||||
test_status = True
|
||||
test_url = '-this-is-a-test/for_realz'
|
||||
test_temp = camera.temperature + 7
|
||||
test_battery = camera.battery - 1
|
||||
test_notif = camera.notifications - 1
|
||||
values = {'name':test_name, 'armed':test_status, 'thumbnail':test_url, 'temp':test_temp, 'battery':test_battery, 'notifications':test_notif}
|
||||
|
||||
camera.update(values)
|
||||
# Check that values are properly stored and can be recalled
|
||||
jpg_url = BASE_URL + test_url +'.jpg'
|
||||
mp4_url = BASE_URL + test_url +'.mp4'
|
||||
self.assertEqual(camera.name, test_name)
|
||||
self.assertEqual(camera.armed, test_status)
|
||||
self.assertEqual(camera.clip, mp4_url)
|
||||
self.assertEqual(camera.thumbnail, jpg_url)
|
||||
self.assertEqual(camera.temperature, test_temp)
|
||||
self.assertEqual(camera.battery, test_battery)
|
||||
self.assertEqual(camera.notifications, test_notif)
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
ISONLINE = True
|
||||
ARMED = True
|
||||
ARMED2 = False
|
||||
REGION_ID = 'test'
|
||||
REGION = 'Oceaniaeurasia'
|
||||
TOKEN = 'abcd1234$$@@'
|
||||
NETWORK_ID = 1337
|
||||
DEVICE_ID = 9876
|
||||
DEVICE_ID2 = 6789
|
||||
ACCOUNT_ID = 1234
|
||||
CAMERA_NAME = 'My Camera'
|
||||
CAMERA_NAME2 = 'Number 2'
|
||||
BATTERY = 3
|
||||
BATTERY2 = 1
|
||||
TEMP = 70
|
||||
TEMP2 = 66
|
||||
THUMB = '/url/camera/7777/clip'
|
||||
THUMB2 = '/url/camera/8888/clip'
|
||||
NOTIFS = 1
|
||||
NOTIFS2 = 1
|
||||
|
||||
if ISONLINE:
|
||||
ONLINE = 'online'
|
||||
else:
|
||||
ONLINE = 'offline'
|
||||
|
||||
|
||||
response = {'account': {'notifications': 1},
|
||||
'devices': [{'device_type': 'camera',
|
||||
'notifications': NOTIFS,
|
||||
'battery': BATTERY,
|
||||
'active': 'disabled',
|
||||
'errors': 0,
|
||||
'error_msg': '',
|
||||
'enabled': False,
|
||||
'temp': TEMP,
|
||||
'updated_at': '2017-01-27T03:14:24+00:00',
|
||||
'lfr_strength': 3,
|
||||
'armed': ARMED,
|
||||
'device_id': DEVICE_ID,
|
||||
'wifi_strength': 5,
|
||||
'warning': 0,
|
||||
'thumbnail': THUMB,
|
||||
'name': CAMERA_NAME,
|
||||
'status': 'done'},
|
||||
{'device_type': 'camera',
|
||||
'notifications': NOTIFS2,
|
||||
'battery': BATTERY2,
|
||||
'active': 'disabled',
|
||||
'errors': 0,
|
||||
'error_msg': '',
|
||||
'enabled': False,
|
||||
'temp': TEMP2,
|
||||
'updated_at': '2017-01-27T03:14:24+00:00',
|
||||
'lfr_strength': 3,
|
||||
'armed': ARMED2,
|
||||
'device_id': DEVICE_ID2,
|
||||
'wifi_strength': 5,
|
||||
'warning': 0,
|
||||
'thumbnail': THUMB2,
|
||||
'name': CAMERA_NAME2,
|
||||
'status': 'done'},
|
||||
{'updated_at': '2017-01-26T19:32:10+00:00',
|
||||
'device_type': 'sync_module',
|
||||
'notifications': 0,
|
||||
'device_id': 0000,
|
||||
'status':
|
||||
'online',
|
||||
'last_hb':
|
||||
'2017-01-27T03:14:49+00:00',
|
||||
'errors': 0,
|
||||
'error_msg': '',
|
||||
'warning': 0}],
|
||||
'network': {'armed': ARMED,
|
||||
'wifi_strength': 5,
|
||||
'warning': 0,
|
||||
'error_msg': '',
|
||||
'name': 'Blink',
|
||||
'notifications': NOTIFS,
|
||||
'status': 'ok'},
|
||||
'events': [{'camera_id':DEVICE_ID,
|
||||
'type':'motion',
|
||||
'video_url':THUMB + '.mp4',
|
||||
'created_at':'2017-01-01'},
|
||||
{'camera_id':DEVICE_ID2,
|
||||
'type':'None'}],
|
||||
'syncmodule':{'name':'SyncName',
|
||||
'status':ONLINE}}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import testtools
|
||||
import blinkpy
|
||||
|
||||
class TestImport(testtools.TestCase):
|
||||
def test_import(self):
|
||||
pass
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Tests camera and system functions."""
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
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
|
||||
|
||||
|
||||
class TestBlinkSyncModule(unittest.TestCase):
|
||||
"""Test BlinkSyncModule functions in blinkpy."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up Blink module."""
|
||||
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.camera = BlinkCamera(self.config, self.blink.sync)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after test."""
|
||||
self.blink = None
|
||||
self.config = {}
|
||||
self.camera = 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')
|
||||
|
||||
@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_video_count(self):
|
||||
"""Test video count function."""
|
||||
self.blink.sync.return_value = {'count': 1}
|
||||
self.assertEqual(self.blink.sync.video_count, 1)
|
||||
@@ -1,23 +1,41 @@
|
||||
[tox]
|
||||
envlist = py34, pep8
|
||||
envlist = build, py34, py35, py36, py37, lint
|
||||
skip_missing_interpreters = True
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
setenv =
|
||||
LAND=en_US.UTF-8
|
||||
LANG=en_US.UTF-8
|
||||
PYTHONPATH = {toxinidir}
|
||||
commands =
|
||||
py.test -v --timeout=30 --duration=10 --cov=blinkpy --cov-report html {posargs}
|
||||
py.test --timeout=30 --duration=10 --cov=blinkpy --cov-report term-missing {posargs}
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/requirements_test.txt
|
||||
|
||||
[testenv:pep8]
|
||||
[testenv:lint]
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
flake8
|
||||
-r{toxinidir}/requirements_test.txt
|
||||
basepython = python3
|
||||
ignore_errors = True
|
||||
commands =
|
||||
flake8 blinkpy.py
|
||||
|
||||
[flake8]
|
||||
ignore = E501
|
||||
exclude = .venv,.git,.tox,dist,doc,*lib/python,*egg,build
|
||||
pylint --rcfile={toxinidir}/pylintrc blinkpy tests
|
||||
flake8 blinkpy tests
|
||||
pydocstyle blinkpy tests
|
||||
rst-lint README.rst
|
||||
rst-lint CHANGES.rst
|
||||
|
||||
[testenv:build]
|
||||
recreate = True
|
||||
skip_install = True
|
||||
whitelist_externals =
|
||||
/bin/sh
|
||||
/bin/rm
|
||||
deps =
|
||||
-r{toxinidir}/requirements_test.txt
|
||||
commands =
|
||||
/bin/rm -rf build dist
|
||||
python setup.py bdist_wheel
|
||||
/bin/sh -c "pip install --upgrade dist/*.whl"
|
||||
py.test tests
|
||||
|
||||
Reference in New Issue
Block a user