Compare commits

..
79 Commits
Author SHA1 Message Date
Kevin FronczakandGitHub cfeed4f432 Merge pull request #30 from fronzbot/v0.6.0
v0.6.0
2017-05-12 16:09:45 -04:00
Kevin Fronczak 6ca243c05c Version bump 2017-05-12 16:03:50 -04:00
Kevin FronczakandGitHub cfa2f0f1bc Merge pull request #29 from fronzbot/better-battery-report
Added battery_string property to camera
2017-05-12 15:53:00 -04:00
Kevin Fronczak d4456555b3 Updated README 2017-05-12 15:46:53 -04:00
Kevin Fronczak 01babca0f4 Added battery_string property to camera
Ability to detect if battery is "OK" or "Low"
2017-05-12 15:42:18 -04:00
Kevin FronczakandGitHub 123444ddf6 Merge pull request #27 from fronzbot/reauth-tests
Reauth tests
2017-05-12 15:08:44 -04:00
Kevin Fronczak aeeb894ad9 Added reauth test 2017-05-12 14:46:13 -04:00
Kevin Fronczak d290fe4096 Added test for reauth
Test to check that we throw an exception if we cannot reauth (to ensure we don't get stuck in a loop)
2017-05-12 14:03:35 -04:00
Kevin FronczakandGitHub 565e3e1b9d Merge pull request #24 from tySwift93/development
Add Token Reauthorization
2017-05-12 10:39:27 -04:00
tySwift93andGitHub b23fe8319b Removed a single space to make lint happy once more
PLEASE BE HAPPY
2017-05-10 19:47:22 -04:00
tySwift93andGitHub eb41b4285e Lint is the bane of my existence 2017-05-10 19:42:20 -04:00
tySwift93andGitHub ab684f64ff Lint is killing me 2017-05-10 19:27:12 -04:00
tySwift93andGitHub abea28ef00 Undo my attempt at fixing code coverage 2017-05-10 19:23:26 -04:00
tySwift93andGitHub 8934645c4d One final attempt to get coverage to work...
I'm just hoping something sticks!
2017-05-10 19:20:26 -04:00
tySwift93andGitHub 68d64665a6 Trying to fix test coverage really quickly... 2017-05-10 19:03:39 -04:00
tySwift93andGitHub 5099d0e67e Update the changes made to _request method
Forgot to include 'return'
2017-05-10 18:51:34 -04:00
tySwift93andGitHub 7a3ba3ae03 Minor change that will hopefully trigger Hound 2017-05-10 10:18:27 -04:00
Kevin FronczakandGitHub 80272b1e27 Merge pull request #25 from fronzbot/hound
Create .hound.yml
2017-05-10 09:11:42 -04:00
Kevin FronczakandGitHub 8d807289ae Create .hound.yml 2017-05-10 09:04:56 -04:00
ttrushin cb946209a2 Has it become obvious that I have never used pylint before?...Please work now... 2017-05-09 16:09:44 -04:00
ttrushin 890b58d0ce Another attempt to fix pylint-related issues 2017-05-09 16:01:31 -04:00
ttrushin 6623f2559d Hopefully fixed pylint-related issues 2017-05-09 15:46:18 -04:00
ttrushin b395eba4f3 Fixed misspelling yet again... 2017-05-09 15:28:03 -04:00
ttrushin c13f3d5b77 Fixed tests, fixed misspelling, and fixed a small error in _request method 2017-05-09 15:15:31 -04:00
ttrushin 13c7373917 Added auto-reauthorization (token refresh) when a request fails due to an expired token 2017-05-09 14:47:31 -04:00
Kevin FronczakandGitHub b0cb3dda37 Merge pull request #22 from fronzbot/dev
Merge 0.6.0.dev0 branch
2017-04-15 17:38:36 -04:00
Kevin FronczakandGitHub a0ab6700d8 Update CHANGES.rst 2017-03-21 10:01:24 -04:00
Kevin FronczakandGitHub ebf4cb53b5 Merge pull request #21 from fronzbot/cleanup
Cleanup and Coverage Increase
2017-03-20 15:57:43 -04:00
Kevin Fronczak 9fa2e967dc Removed redundant functions, increased test coverage 2017-03-20 15:53:53 -04:00
Kevin Fronczak f853b410e7 Merge branch 'dev' of github.com:fronzbot/blinkpy into cleanup
Conflicts:
	helpers/constants.py
2017-03-20 14:06:35 -04:00
Kevin Fronczak 874f717bd1 Fixed formatting 2017-03-20 14:05:40 -04:00
Kevin Fronczak 479a41c900 Had to update version due to messing up the module distribution 2017-03-20 14:05:39 -04:00
Kevin Fronczak a0a1e2837c Fixed coveralls badge 2017-03-20 14:05:39 -04:00
Kevin FronczakandGitHub bd783385de Merge pull request #20 from fronzbot/fronzbot-api-formatting
Fixed formatting
2017-03-20 13:37:23 -04:00
Kevin FronczakandGitHub 5f19b3e47b Fixed formatting 2017-03-20 13:22:12 -04:00
Kevin FronczakandGitHub c7fe0a5b5c Merge pull request #19 from fronzbot/constant-fix
Fixed copy-paste error
2017-03-14 15:11:38 -04:00
Kevin FronczakandGitHub 5eb1b663f4 Fixed copy-paste error 2017-03-14 15:11:22 -04:00
Kevin FronczakandGitHub 1e194385b8 Merge pull request #18 from fronzbot/dev-version-bump
Bump version to dev
2017-03-14 15:10:12 -04:00
Kevin FronczakandGitHub 731fb87012 Bump version to dev 2017-03-14 15:09:35 -04:00
Kevin FronczakandGitHub 5e1634dbfa Merge pull request #17 from fronzbot/master
Pulling in updates from master to dev branch
2017-03-14 15:07:56 -04:00
Kevin Fronczak 7d366386da Had to update version due to messing up the module distribution 2017-03-12 22:06:39 -04:00
Kevin Fronczak b4a03778d3 Fixed coveralls badge 2017-03-12 20:04:42 -04:00
Kevin FronczakandGitHub 657924532e Merge pull request #16 from fronzbot/dev
0.5.0
2017-03-12 19:56:02 -04:00
Kevin Fronczak 9099909983 Bumping release to 0.5.0 2017-03-12 19:51:28 -04:00
Kevin FronczakandGitHub 9cf94ceb86 Merge pull request #15 from fronzbot/robust_regions
Added support for rest.piri subdomain
2017-03-12 19:49:06 -04:00
Kevin Fronczak c529f474fe Added support for rest.piri subdomain 2017-03-12 19:45:33 -04:00
Kevin Fronczak e31a5979bb Fixed 2017-03-12 18:45:52 -04:00
Kevin FronczakandGitHub 90ee45349c Merge pull request #14 from fronzbot/better_tests
Better tests, proper region handling
2017-03-12 18:38:33 -04:00
Kevin Fronczak ab7305b7b2 Fixed typo 2017-03-12 18:26:23 -04:00
Kevin Fronczak af0cdb006a Completed test suite. Fixed some minor issues with file writing and regions 2017-03-12 18:21:28 -04:00
Kevin Fronczak 326906f073 Getting there... 2017-03-11 23:21:26 -05:00
Kevin Fronczak d54a6cbb71 Started adding more camera tests, reorganized package 2017-03-11 21:53:58 -05:00
Kevin Fronczak 94dab01e6b Blink tests complete, camera module next 2017-03-11 12:33:11 -05:00
Kevin Fronczak afa7b2259f Fixed travis to run lint instead of pep8 and moved test reqs to own file 2017-03-10 14:33:51 -05:00
Kevin Fronczak dad1aec874 Initial checkin for test_blink_system.py 2017-03-09 22:50:54 -05:00
Kevin Fronczak 14c7322cde Removed leftover merge junk 2017-03-08 23:19:06 -05:00
Kevin Fronczak 7b07146afa Constants and better style testing 2017-03-08 23:11:56 -05:00
Kevin Fronczak 296c21fd3f Revert "Added constants and error file to clean stuff up"
This reverts commit 69b7be744a.
2017-03-08 23:09:57 -05:00
Kevin Fronczak 5215d0f298 Revert "More robust style testing... next up is more robust testing"
This reverts commit daa27e4989.
2017-03-08 23:09:41 -05:00
Kevin Fronczak daa27e4989 More robust style testing... next up is more robust testing 2017-03-08 23:03:09 -05:00
Kevin Fronczak 69b7be744a Added constants and error file to clean stuff up 2017-03-08 22:00:28 -05:00
Kevin Fronczak 7fa1353173 Working on updating constants 2017-03-08 21:26:28 -05:00
Kevin Fronczak 3653e9d63d Merge branch 'master' of github.com:fronzbot/blinkpy into patch
Conflicts:
	blinkpy.py
	tests/test_const.py
2017-03-08 21:00:21 -05:00
Kevin Fronczak 0a2e087eaa Working on updating with constants. This commit is broken 2017-03-08 20:57:38 -05:00
Kevin Fronczak 11b683a156 Fixed issue with host name in header, added test to catch this 2017-03-08 20:57:38 -05:00
Kevin Fronczak d387a40154 Changed to wheel and incremented minor version 2017-03-08 20:57:38 -05:00
Kevin Fronczak 0023be40c1 Updated test_const with realistic response and fixed key error in blinkpy 2017-03-08 20:57:38 -05:00
Kevin Fronczak 529db79ea4 Added a thumb dict function 2017-03-08 20:57:38 -05:00
Kevin Fronczak 32a27f2d98 Added image refresh function, increased test coverage 2017-03-08 20:57:38 -05:00
Kevin Fronczak 09f2e29f27 String cast for refresh 2017-03-08 20:57:38 -05:00
Kevin Fronczak fa4a1a4e8c Fixed refresh function 2017-03-08 20:57:38 -05:00
Kevin FronczakandGitHub 8206a49dc7 Update README.rst 2017-03-06 23:39:38 -05:00
Kevin FronczakandGitHub 6ad66f0d14 Update README.rst 2017-03-06 23:36:49 -05:00
Kevin FronczakandGitHub 6399f43677 Update README.rst 2017-03-06 23:36:29 -05:00
Kevin FronczakandGitHub 3a5c85e6dd Update .travis.yml 2017-03-06 23:32:08 -05:00
Kevin FronczakandGitHub ac440d814e Added coverage badge 2017-03-06 23:29:09 -05:00
Kevin FronczakandGitHub 270f2f8de4 Create .travis.yml 2017-03-06 23:28:00 -05:00
Kevin FronczakandGitHub 8ad03fd30b Merge pull request #9 from fronzbot/patch
Fixed issue with host name in header, added test to catch this
2017-03-06 23:21:38 -05:00
Kevin Fronczak 10837e3841 Fixed issue with host name in header, added test to catch this 2017-03-06 23:20:46 -05:00
31 changed files with 1691 additions and 749 deletions
+5
View File
@@ -0,0 +1,5 @@
[run]
omit =
helpers/*
tests/*
setup.py
+9
View File
@@ -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
+1 -2
View File
@@ -6,6 +6,5 @@ htmlcov/*
*.pyc
*.egg*/*
dist/*
MANIFEST
README
.sh
build/*
+2
View File
@@ -0,0 +1,2 @@
python:
enabled: true
+18
View File
@@ -0,0 +1,18 @@
matrix:
fast_finish: true
include:
- python: "3.4.2"
env: TOXENV=py34
- python: "3.4.2"
env: TOXENV=lint
- python: "3.5"
env: TOXENV=py35
- python: "3.6"
env: TOXENV=py36
- python: "3.6-dev"
env: TOXENV=py36
install: pip install -U tox coveralls
language: python
script: tox
after_success: coveralls
+287
View File
@@ -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.
+55
View File
@@ -0,0 +1,55 @@
Changelog
-----------
A list of changes between each release
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
+83
View File
@@ -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.
View File
+4
View File
@@ -0,0 +1,4 @@
include README.rst
include LICENSE.md
include API.md
include tests/*.py
+81 -82
View File
@@ -1,9 +1,9 @@
**blinkpy**
blinkpy |Build Status| |Coverage Status|
============
A Python library for the Blink Camera system
**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,114 +11,113 @@ 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**
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')
.. code:: python
import blinkpy
blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD')
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()
.. code:: python
import blinkpy
blink = blinkpy.Blink()
blink.login()
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()
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
.. code:: python
import blinkpy
blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD')
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.battery_string) # Gives battery level as a string ("OK" or "Low"). Returns "Unknown" if value is... well, unknown
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
**class Blink**
---------------
The following properties/methods are availiable to the Blink class
Class Descriptions
===================
.. code:: python
**Blink.cameras**
Returns a list of BlinkCamera objects found by the system
class Blink()
**Blink.network_id**
Returns the current network id
* ``Blink.cameras`` Returns a dictionary of ``BlinkCamera`` objects where the key corresponds to the camera name and the value is the actual BlinkCamera object.
* ``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 ``BlinkCamera`` objects to represent 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:
.. code:: python
**Blink.account_id**
Returns the account id
Blink.get_auth_token()
Blink.get_ids()
Blink.get_camers()
Blink.set_links()
**Blink.events**
Returns a list of events recorded by blink. This information will contain links to any motion caught by an armed camera.
.. code:: python
**Blink.online**
Returns online status of sync module (True = online, False = offline)
class BlinkCamera(config, urls)
The ``BlinkCamera`` class expects to receive:
**Blink.last_motion()**
Finds last motion information for each camera and stores it in the BlinkCamera.motion field
* A dictionary ``config`` that contains the camera name, device id, armed status, thumbnail url, camera temperature, camery battery level, number of notifications, and region id
* A ``BlinkURLHandler`` object that contains all the links necessary for communication.
**Blink.arm**
Set to True to arm, False to disarm. Can be used to see the status of the system as well
Ultimately, 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):
**Blink.refresh()**
Forces a refresh of all camera information
* ``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'
* ``BlinkCamera.image_refresh()`` Refreshes the current thumbnail.
**Blink.get_summary()**
Returns json formatted summary of the system
.. code:: python
**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'
class BlinkURLHandler(region_id)
The ``BlinkURLHandler`` class expects to be initialized with the region id found in the ``Blink.get_auth_token()`` function. The class will then create the necessary links required for various communication.
.. |Build Status| image:: https://travis-ci.org/fronzbot/blinkpy.svg?branch=master
:target: https://travis-ci.org/fronzbot/blinkpy
.. |Coverage Status| image:: https://coveralls.io/repos/github/fronzbot/blinkpy/badge.svg?branch=master
:target: https://coveralls.io/github/fronzbot/blinkpy?branch=master
+5 -1
View File
@@ -1 +1,5 @@
from blinkpy import Blink
"""Init file for blinkpy."""
from blinkpy.blinkpy import Blink
from blinkpy.blinkpy import BlinkAuthenticationException
from blinkpy.blinkpy import BlinkException
from blinkpy.blinkpy import BlinkURLHandler
+227 -254
View File
@@ -1,352 +1,313 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
blinkpy by Kevin Fronczak - A Blink camera Python library.
'''
blinkpy by Kevin Fronczak - A Blink camera Python library
https://github.com/fronzbot/blinkpy
Original protocol hacking by MattTW : https://github.com/MattTW/BlinkMonitorProtocol
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.
"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
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')
import getpass
from shutil import copyfileobj
import requests
import helpers.errors as ERROR
from helpers.constants import (BLINK_URL, LOGIN_URL,
LOGIN_BACKUP_URL,
DEFAULT_URL, ONLINE)
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)
def _attempt_reauthorization(blink):
"""Attempt to refresh auth token and links."""
headers = blink.get_auth_token()
blink.set_links()
return headers
def _request(blink, url='http://google.com', data=None, headers=None,
reqtype='get', stream=False, json_resp=True, is_retry=False):
"""Wrapper function for request."""
if reqtype == 'post':
response = requests.post(url, headers=headers,
data=data)
elif reqtype == 'get':
response = requests.get(url, headers=headers,
stream=stream)
else:
raise ValueError("Cannot perform requests of type " + type)
raise BlinkException(ERROR.REQUEST)
if json and 'message' in response.keys():
raise BlinkAuthenticationException(response['code'], response['message'])
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 _request(blink, url=url, data=data, headers=headers,
reqtype=reqtype, stream=stream,
json_resp=json_resp, is_retry=True)
return response
if json_resp:
return response.json()
else:
return response
# pylint: disable=super-init-not-called
class BlinkException(Exception):
def __init__(self, id, message):
self.id = id
self.message = message
"""Class to throw general blink exception."""
def __init__(self, errcode):
"""Initialize BlinkException."""
self.errid = errcode[0]
self.message = errcode[1]
class BlinkAuthenticationException(BlinkException):
"""Class to throw authentication exception."""
pass
class BlinkURLHandler(object):
"""Class that handles Blink URLS."""
def __init__(self, region_id):
"""Initialize the urls."""
self.base_url = 'https://' + region_id + '.' + BLINK_URL
self.home_url = self.base_url + '/homescreen'
self.event_url = self.base_url + '/events/network/'
self.network_url = self.base_url + '/network/'
self.networks_url = self.base_url + '/networks'
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']
"""Class to initialize individual camera."""
@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
def __init__(self, config, blink):
"""Initiailize BlinkCamera."""
self.blink = blink
self.urls = self.blink.urls
self.id = str(config['device_id']) # pylint: disable=invalid-name
self.name = config['name']
self._status = config['armed']
self.thumbnail = self.urls.base_url + config['thumbnail'] + '.jpg'
self.clip = self.urls.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 armed(self):
return self._STATUS
"""Return camera arm status."""
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 battery_string(self):
"""Return string indicating battery status."""
if self.battery > 1 and self.battery <= 3:
return "OK"
elif self.battery >= 0:
return "Low"
else:
return "Unknown"
def snap_picture(self):
"""Takes a picture with camera to create a new thumbnail"""
_request(self._IMAGE_LINK, headers=self._HEADER, type='post')
"""Take a picture with camera to create a new thumbnail."""
_request(self.blink, url=self.image_link,
headers=self.header, reqtype='post')
def set_motion_detect(self, enable):
"""Sets motion detection"""
url = self._ARM_LINK
"""Set motion detection."""
url = self.arm_link
if enable:
_request(url + 'enable', headers=self._HEADER, type='post')
_request(self.blink, url=url + 'enable',
headers=self.header, reqtype='post')
else:
_request(url + 'disable', headers=self._HEADER, type='post')
_request(self.blink, url=url + 'disable',
headers=self.header, reqtype='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']
"""Update camera information."""
self.name = values['name']
self._status = values['armed']
self.thumbnail = self.urls.base_url + values['thumbnail'] + '.jpg'
self.clip = self.urls.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']
"""Refresh current thumbnail."""
url = self.urls.home_url
response = _request(self.blink, url=url, headers=self.header,
reqtype='get')['devices']
for element in response:
try:
if str(element['device_id']) == self._ID:
self._THUMB = BASE_URL + element['thumbnail'] + '.jpg'
return self._THUMB
if str(element['device_id']) == self.id:
self.thumbnail = (self.urls.base_url +
element['thumbnail'] + '.jpg')
return self.thumbnail
except KeyError:
pass
return None
def image_to_file(self, path):
"""Write image to file."""
thumb = self.image_refresh()
response = _request(thumb, headers=self._HEADER, stream=True, json=False)
response = _request(self.blink, url=thumb, headers=self.header,
reqtype='get', stream=True, json_resp=False)
if response.status_code == 200:
with open(path, 'wb') as f:
for chunk in response:
f.write(chunk)
with open(path, 'wb') as imgfile:
copyfileobj(response.raw, imgfile)
class Blink(object):
"""Class to initialize communication and sync module"""
"""Class to initialize communication and sync module."""
def __init__(self, username=None, password=None):
"""Constructor for class"""
"""Initialize Blink system."""
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
self._token = None
self._auth_header = None
self.network_id = None
self.account_id = None
self.region = None
self.region_id = None
self._host = None
self._events = []
self.cameras = {}
self._idlookup = {}
self.urls = None
@property
def camera_thumbs(self):
"""Return camera thumbnails."""
self.refresh()
data = {}
for name, camera in self._CAMERAS.items():
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
"""Return id/camera pairs."""
return self._idlookup
@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')['event']
return self._EVENTS
"""Get all events on server."""
url = self.urls.event_url + self.network_id
headers = self._auth_header
self._events = _request(self, url=url, headers=headers,
reqtype='get')['event']
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']]
"""Return boolean system online status."""
url = self.urls.network_url + self.network_id + '/syncmodules'
headers = self._auth_header
return ONLINE[_request(self, url=url, headers=headers,
reqtype='get')['syncmodule']['status']]
def last_motion(self):
"""Finds last motion of each camera"""
"""Find 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]
camera_name = self.id_table[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']}
url = self.urls.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 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"""
"""Arm or disarm system."""
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')
url = self.urls.network_url + self.network_id + '/' + value_to_append
_request(self, url=url, headers=self._auth_header, reqtype='post')
def refresh(self):
"""Gets all blink cameras and pulls their most recent status"""
"""Get all blink cameras and pulls their most recent status."""
response = self.get_summary()['devices']
for name, camera in self._CAMERAS.items():
for name in self.cameras:
camera = self.cameras[name]
for element in response:
try:
if str(element['device_id']) == camera.id:
camera.update(element)
except KeyError:
pass
return None
def get_summary(self):
"""Gets a full summary of device information"""
url = BASE_URL + '/homescreen'
headers = self._AUTH_HEADER
"""Get a full summary of device information."""
url = self.urls.home_url
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?")
if self._auth_header is None:
raise BlinkException(ERROR.AUTH_TOKEN)
return _request(url, headers=headers, type='get')
return _request(self, url=url, headers=headers, reqtype='get')
def get_cameras(self):
"""Finds and creates cameras"""
"""Find 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
if ('device_type' in element and
element['device_type'] == 'camera'):
# Add region to config
element['region_id'] = self.region_id
device = BlinkCamera(element, self)
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 + '/'
"""Set access links and required headers for each camera in system."""
for name in self.cameras:
camera = self.cameras[name]
network_id_url = self.urls.network_url + self.network_id
image_url = network_id_url + '/camera/' + camera.id + '/thumbnail'
arm_url = network_id_url + '/camera/' + camera.id + '/'
camera.image_link = image_url
camera.arm_link = arm_url
camera.header = self._AUTH_HEADER
camera.header = self._auth_header
def setup_system(self):
"""Method logs in and sets auth token and network ids for future requests"""
"""
Wrapper for various setup functions.
Method logs in and sets auth token, urls, and 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")
raise BlinkAuthenticationException(ERROR.AUTHENTICATE)
self.get_auth_token()
self.get_ids()
@@ -354,41 +315,53 @@ class Blink(object):
self.set_links()
def login(self):
"""Prompts user for username and password"""
"""Prompt 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"""
"""Retrieve the authentication token from Blink."""
if not isinstance(self._username, str):
raise BlinkAuthenticationException(0, "Username must be a string")
raise BlinkAuthenticationException(ERROR.USERNAME)
if not isinstance(self._password, str):
raise BlinkAuthenticationException(0, "Password must be a string")
raise BlinkAuthenticationException(ERROR.PASSWORD)
headers = {'Host': DEFAULT_URL,
'Content-Type': 'application/json'
}
'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
}
response = _request(self, url=LOGIN_URL, headers=headers,
data=data, json_resp=False, reqtype='post')
if response.status_code is 200:
response = response.json()
(self.region_id, self.region), = response['region'].items()
else:
response = _request(self, url=LOGIN_BACKUP_URL, headers=headers,
data=data, reqtype='post')
self.region_id = 'rest.piri'
self.region = "UNKNOWN"
self._host = 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):
"""Sets the network ID and Account ID"""
url = BASE_URL + '/networks'
headers = self._AUTH_HEADER
"""Set the network ID and Account ID."""
url = self.urls.networks_url
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?")
if self._auth_header is None:
raise BlinkException(ERROR.AUTH_TOKEN)
response = _request(url, headers=headers, type='get')
self._NETWORKID = str(response['networks'][0]['id'])
self._ACCOUNTID = str(response['networks'][0]['account_id'])
response = _request(self, url=url, headers=headers, reqtype='get')
self.network_id = str(response['networks'][0]['id'])
self.account_id = str(response['networks'][0]['account_id'])
-22
View File
@@ -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))
-18
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
"""Init file for blinkpy helper functions."""
from helpers import constants
from helpers import errors
+56
View File
@@ -0,0 +1,56 @@
'''
constants.py
Generates constants for use in blinkpy
'''
import os
MAJOR_VERSION = 0
MINOR_VERSION = 6
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}
+6
View File
@@ -0,0 +1,6 @@
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 logged in and received your token?")
REQUEST = (4, "Cannot perform request (get/post type incorrect)")
+23
View File
@@ -0,0 +1,23 @@
[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-*
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,
+5
View File
@@ -0,0 +1,5 @@
flake8==3.3
pylint==1.6.5
pydocstyle==1.1.1
pytest>=2.9.2
pytest-sugar>=0.8.0
+20 -22
View File
@@ -1,26 +1,24 @@
# -*- coding: utf-8 -*-
from setuptools import setup
from setuptools import setup, find_packages
from helpers.constants import (__version__, PROJECT_PACKAGE_NAME,
PROJECT_LICENSE, PROJECT_URL,
PROJECT_EMAIL, PROJECT_DESCRIPTION,
PROJECT_CLASSIFIERS, PROJECT_AUTHOR,
PROJECT_LONG_DESCRIPTION)
setup(
name = 'blinkpy',
version = '0.4.',
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>=2,<3'],
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'
]
name = PROJECT_PACKAGE_NAME,
version = __version__,
description = PROJECT_DESCRIPTION,
long_description = PROJECT_LONG_DESCRIPTION,
author = PROJECT_AUTHOR,
author_email = PROJECT_EMAIL,
license = PROJECT_LICENSE,
url = PROJECT_URL,
platforms = 'any',
py_modules = ['blinkpy'],
packages=find_packages(),
install_requires = ['requests>=2,<3'],
test_suite = 'tests',
classifiers = PROJECT_CLASSIFIERS
)
+1
View File
@@ -0,0 +1 @@
"""Init file for tests directory."""
+306
View File
@@ -0,0 +1,306 @@
"""
Mock responses that mimic actual responses from Blink servers.
This file should be updated any time the Blink server responses
change so we can make sure blinkpy can still communicate.
"""
import helpers.constants as const
NETWORKS_RESPONSE = {}
NETWORKS_RESPONSE['summary'] = {'onboarded': True, 'name': 'Nilfgaard'}
NETWORKS_RESPONSE['networks'] = [{
'network_key': None,
'name': NETWORKS_RESPONSE['summary']['name'],
'account_id': 1989,
'id': 9898,
'encryption_key': None,
'armed': True,
'ping_interval': 60,
'video_destination': 'server',
'arm_string': 'Armed',
'feature_plan_id': None
}]
NEW_THUMBNAIL = '/NEW/THUMBNAIL/YAY'
FIRST_CAMERA = {'device_type': 'camera',
'notifications': 1,
'battery': 2,
'active': 'enabled',
'enabled': True,
'temp': 70,
'updated_at': '2017-01-01T01:23:45+00:00',
'lfr_strength': 3,
'armed': True,
'device_id': 2112,
'wifi_strength': 5,
'thumbnail': '/this/is/url1/thumb',
'name': 'First Camera',
'status': 'done'}
SECOND_CAMERA = {'device_type': 'camera',
'notifications': 0,
'battery': 3,
'active': 'disabled',
'enabled': False,
'temp': 82,
'updated_at': '2017-12-21T01:21:12+00:00',
'lfr_strength': 1,
'armed': False,
'device_id': 1221,
'wifi_strength': 1,
'thumbnail': '/this/is/url2/thumb',
'name': 'Second Camera',
'status': 'done'}
SYNC_MODULE = {'updated_at': '1970-01-01T01:00:00+00:00',
'device_type': 'sync_module',
'notifications': 2,
'device_id': 7990,
'status': 'online'}
FIRST_EVENT = {'camera_name': FIRST_CAMERA['name'],
'updated_at': '2017-10-10T03:37:37+00:00',
'sync_module_id': None,
'camera': FIRST_CAMERA['device_id'],
'type': 'motion',
'duration': None,
'status': None,
'created_at': '2017-01-28T19:51:52+00:00',
'camera_id': FIRST_CAMERA['device_id'],
'id': 666777888,
'siren_id': None,
'account_id': NETWORKS_RESPONSE['networks'][0]['account_id'],
'notified': True,
'siren': None,
'syncmodule': None,
'video_url': FIRST_CAMERA['thumbnail'] + '.mp4',
'command_id': None,
'network_id': None,
'account': NETWORKS_RESPONSE['networks'][0]['account_id'],
'video_id': 123000321}
SECOND_EVENT = {'updated_at': '2017-01-28T19:51:29+00:00',
'sync_module_id': SYNC_MODULE['device_id'],
'camera': None,
'type': 'armed',
'duration': None,
'status': None,
'created_at': '2017-01-28T19:51:29+00:00',
'camera_id': None,
'id': 333777555,
'siren_id': None,
'account_id': NETWORKS_RESPONSE['networks'][0]['account_id'],
'notified': False,
'siren': None,
'syncmodule': SYNC_MODULE['device_id'],
'command_id': None,
'network_id': None,
'account': NETWORKS_RESPONSE['networks'][0]['account_id']}
"""Fake response content."""
LOGIN_RESPONSE = {}
LOGIN_RESPONSE['region'] = {'ciri': 'Cintra'}
LOGIN_RESPONSE['networks'] = {
NETWORKS_RESPONSE['networks'][0]['id']: NETWORKS_RESPONSE['summary']
}
LOGIN_RESPONSE['authtoken'] = {'authtoken': 'foobar7117', 'message': 'auth'}
RESPONSE = {}
RESPONSE['account'] = {'notifications': SYNC_MODULE['notifications']}
RESPONSE['devices'] = [FIRST_CAMERA, SECOND_CAMERA, SYNC_MODULE]
RESPONSE['network'] = {'armed': NETWORKS_RESPONSE['networks'][0]['armed'],
'wifi_strength': 4,
'warning': 0,
'name': NETWORKS_RESPONSE['summary']['name'],
'notifications': SYNC_MODULE['notifications']}
RESPONSE['event'] = [FIRST_EVENT, SECOND_EVENT]
RESPONSE['syncmodule'] = {'name': 'Vengerberg', 'status': 'online'}
BAD_RESPONSE = {}
BAD_RESPONSE['account'] = {'nothing': 'here'}
BAD_RESPONSE['devices'] = [{'foo': 'bar', 'device_type': 'camera'},
{'device_type': 'camera', 'device_id': 2112}]
BAD_RESPONSE['network'] = {'bar': 'foo'}
MOCK_BYTES = '\x00\x10JFIF\x00\x01'
IMAGE_TO_WRITE_URL = list()
IMAGE_TO_WRITE_URL.append('https://ciri.' + const.BLINK_URL +
FIRST_CAMERA['thumbnail'] + '.jpg')
IMAGE_TO_WRITE_URL.append('https://ciri.' + const.BLINK_URL +
SECOND_CAMERA['thumbnail'] + '.jpg')
FAKE_FILES = list()
def mocked_requests_post(*args, **kwargs):
"""Mock post request."""
class MockPostResponse:
"""Class for mock post response."""
def __init__(self, json_data, status_code):
"""Initialze mock post response."""
self.json_data = json_data
self.status_code = status_code
def json(self):
"""Return json data from post request."""
return self.json_data
# pylint: disable=global-variable-not-assigned
global RESPONSE
# pylint: disable=global-variable-not-assigned
global NETWORKS_RESPONSE
if args[0] is not None:
url_tail = args[0].split("/")[-1]
else:
return MockPostResponse({'message': 'ERROR', 'code': 404}, 404)
if args[0] == const.LOGIN_URL:
# Request to login
return MockPostResponse(LOGIN_RESPONSE, 200)
elif args[0] == const.LOGIN_BACKUP_URL:
return MockPostResponse(LOGIN_RESPONSE, 200)
elif url_tail == 'arm' or url_tail == 'disarm':
# Request to arm/disarm system
NETWORKS_RESPONSE['networks'][0]['armed'] = url_tail == 'arm'
RESPONSE['network']['armed'] = url_tail == 'arm'
return MockPostResponse({}, 200)
elif url_tail == 'enable' or url_tail == 'disable':
# Request to enable/disable motion detection per camera
received_id = args[0].split("/")[-2]
all_devices = list()
for element in RESPONSE['devices']:
all_devices.append(element)
current_id = element['device_id']
if str(current_id) == received_id:
element['armed'] = url_tail == 'enable'
element['enabled'] = url_tail == 'enable'
RESPONSE['devices'] = all_devices
return MockPostResponse({}, 200)
elif url_tail == 'thumbnail':
# Requesting a new image
received_id = args[0].split("/")[-2]
all_devices = list()
for element in RESPONSE['devices']:
all_devices.append(element)
if str(element['device_id']) == received_id:
element['thumbnail'] = NEW_THUMBNAIL
RESPONSE['devices'] = all_devices
return MockPostResponse({}, 200)
return MockPostResponse({'message': 'ERROR', 'code': 404}, 404)
def mocked_requests_get(*args, **kwargs):
"""Mock get request."""
class MockGetResponse:
"""Class for mock get 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
rx_header = kwargs.pop('headers')
expected_token = LOGIN_RESPONSE['authtoken']['authtoken']
# pylint: disable=unused-variable
if ('Content-Type' not in rx_header
and rx_header['TOKEN_AUTH'] != expected_token):
return MockGetResponse({'message': 'Not Authorized', 'code': 400}, 400)
(region_id, region), = LOGIN_RESPONSE['region'].items()
if (args[0] != 'use_bad_response'
and args[0] != 'reauth' and args[0] is not None):
set_region_id = args[0].split('/')[2].split('.')[0]
else:
set_region_id = 'ciri'
if set_region_id == 'rest':
set_region_id = (set_region_id + '.' +
args[0].split('/')[2].split('.')[1])
region_id = 'rest.piri'
neturl = 'https://' + set_region_id + '.' + const.BLINK_URL + '/networks'
if args[0] == neturl:
return MockGetResponse(NETWORKS_RESPONSE, 200)
elif set_region_id != region_id:
raise ConnectionError('Received region id ' + region_id +
' Expected ' + set_region_id)
elif args[0] in IMAGE_TO_WRITE_URL:
return MockGetResponse({}, 200, raw_data=MOCK_BYTES)
elif args[0] == 'use_bad_response':
return MockGetResponse(BAD_RESPONSE, 200)
elif args[0] == 'reauth':
return MockGetResponse({'message': 'REAUTH', 'code': 777}, 777)
else:
return MockGetResponse(RESPONSE, 200)
return MockGetResponse({'message': 'ERROR', 'code': 404}, 404)
def mocked_copyfileobj(*args, **kwargs):
"""Mock shutil.copyfileobj."""
class MockCopyFileObj:
"""Class for mock copy file."""
def __init__(self, src, dst):
"""Initialize copyfile mock."""
self.src = src
self.dst = dst
# pylint: disable=global-variable-not-assigned
global FAKE_FILES
mockobj = MockCopyFileObj(args[0], args[1])
FAKE_FILES.append(mockobj.src)
return
def get_test_cameras(base_url):
"""Helper function to return cameras named in this file."""
test_cameras = dict()
for element in RESPONSE['devices']:
if ('device_type' in element and
element['device_type'] == 'camera'):
test_cameras[element['name']] = {
'device_id': str(element['device_id']),
'armed': element['armed'],
'thumbnail': (base_url +
element['thumbnail'] + '.jpg'),
'temperature': element['temp'],
'battery': element['battery'],
'notifications': element['notifications']
}
return test_cameras
def get_test_id_table():
"""Helper function to return mock id table."""
test_id_table = dict()
for element in RESPONSE['devices']:
if ('device_type' in element and
element['device_type'] == 'camera'):
test_id_table[str(element['device_id'])] = element['name']
return test_id_table
class MockURLHandler(object):
"""Mocks URL Handler in blinkpy module."""
def __init__(self, region_id):
"""Initialize the urls."""
self.base_url = 'https://' + region_id + '.' + const.BLINK_URL
self.home_url = 'reauth'
self.event_url = self.base_url + '/events/network/'
self.network_url = self.base_url + '/network/'
self.networks_url = self.base_url + '/networks'
+107
View File
@@ -0,0 +1,107 @@
"""
Tests the camera initialization and attributes of
individual BlinkCamera instantiations.
"""
import unittest
from unittest import mock
import blinkpy
import tests.mock_responses as mresp
USERNAME = 'foobar'
PASSWORD = 'deadbeef'
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)
def tearDown(self):
"""Clean up after test."""
self.blink = None
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
def test_camera_properties(self, mock_get, mock_post):
"""Tests all property set/recall."""
test_value = 'foobar'
test_region_id = list(mresp.LOGIN_RESPONSE['region'].keys())[0]
self.blink.setup_system()
for name in self.blink.cameras:
camera = self.blink.cameras[name]
camera.name = test_value
camera.clip = test_value + '.mp4'
camera.thumbnail = test_value + '.jpg'
camera.temperature = 10
camera.battery = 0
camera.notifications = 100
camera.image_link = test_value + '/image.jpg'
camera.arm_link = test_value + '/arm'
camera.header = {'foo': 'bar'}
camera.motion = {'bar': 'foo'}
self.assertEqual(camera.clip, test_value + '.mp4')
self.assertEqual(camera.name, test_value)
self.assertEqual(camera.thumbnail, test_value + '.jpg')
self.assertEqual(camera.temperature, 10)
self.assertEqual(camera.battery, 0)
self.assertEqual(camera.notifications, 100)
self.assertEqual(camera.image_link, test_value + '/image.jpg')
self.assertEqual(camera.arm_link, test_value + '/arm')
self.assertEqual(camera.header, {'foo': 'bar'})
self.assertEqual(camera.motion, {'bar': 'foo'})
self.assertEqual(camera.region_id, test_region_id)
self.assertEqual(camera.battery_string, "Low")
camera.battery = 3
self.assertEqual(camera.battery_string, "OK")
camera.battery = -10
self.assertEqual(camera.battery_string, "Unknown")
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
def test_camera_values_from_setup(self, mock_get, mock_post):
"""Tests all property values after camera setup."""
self.blink.setup_system()
# Get expected test values
test_network_id = str(mresp.NETWORKS_RESPONSE['networks'][0]['id'])
# pylint: disable=unused-variable
(region_id, region), = mresp.LOGIN_RESPONSE['region'].items()
# pylint: disable=protected-access
expected_header = self.blink._auth_header
test_urls = blinkpy.BlinkURLHandler(region_id)
test_cameras = mresp.get_test_cameras(test_urls.base_url)
test_net_id_url = test_urls.network_url + test_network_id
for name in self.blink.cameras:
camera = self.blink.cameras[name]
self.assertEqual(name, camera.name)
if name in test_cameras:
self.assertEqual(camera.id,
test_cameras[name]['device_id'])
self.assertEqual(camera.armed,
test_cameras[name]['armed'])
self.assertEqual(camera.thumbnail,
test_cameras[name]['thumbnail'])
self.assertEqual(camera.temperature,
test_cameras[name]['temperature'])
self.assertEqual(camera.battery,
test_cameras[name]['battery'])
self.assertEqual(camera.notifications,
test_cameras[name]['notifications'])
else:
self.fail("Camera wasn't initialized: " + name)
expected_arm_link = test_net_id_url + '/camera/' + camera.id + '/'
expected_image_link = expected_arm_link + 'thumbnail'
self.assertEqual(camera.image_link, expected_image_link)
self.assertEqual(camera.arm_link, expected_arm_link)
self.assertEqual(camera.header, expected_header)
+175
View File
@@ -0,0 +1,175 @@
"""Tests camera and system functions."""
import unittest
from unittest import mock
import blinkpy
import tests.mock_responses as mresp
USERNAME = 'foobar'
PASSWORD = 'deadbeef'
class TestBlinkFunctions(unittest.TestCase):
"""Test Blink and BlinkCamera functions in blinkpy."""
def setUp(self):
"""Set up Blink module."""
self.blink = blinkpy.Blink(username=USERNAME,
password=PASSWORD)
(self.region_id, self.region), = mresp.LOGIN_RESPONSE['region'].items()
self.test_urls = blinkpy.BlinkURLHandler(self.region_id)
self.urls = self.test_urls
def tearDown(self):
"""Clean up after test."""
self.blink = None
self.region = None
self.region_id = None
self.test_urls = None
self.urls = None
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
def test_set_motion_detect(self, mock_get, mock_post):
"""Checks if we can set motion detection."""
self.blink.setup_system()
self.test_urls = blinkpy.BlinkURLHandler(self.region_id)
test_cameras = mresp.get_test_cameras(self.test_urls.base_url)
for camera_name in test_cameras:
self.blink.cameras[camera_name].set_motion_detect(True)
self.blink.refresh()
self.assertEqual(self.blink.cameras[camera_name].armed, True)
self.blink.cameras[camera_name].set_motion_detect(False)
self.blink.refresh()
self.assertEqual(self.blink.cameras[camera_name].armed, False)
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
def test_last_motion(self, mock_get, mock_post):
"""Checks that we can get the last motion info."""
self.test_urls = blinkpy.BlinkURLHandler(self.region_id)
test_events = mresp.RESPONSE['event']
test_video = dict()
test_image = dict()
test_time = dict()
for event in test_events:
if event['type'] == 'motion':
url = self.test_urls.base_url + event['video_url']
test_video[event['camera_name']] = url
test_image[event['camera_name']] = url[:-3] + 'jpg'
test_time[event['camera_name']] = event['created_at']
self.blink.setup_system()
for name in self.blink.cameras:
camera = self.blink.cameras[name]
self.blink.last_motion()
if name in test_video:
self.assertEqual(camera.motion['video'], test_video[name])
else:
self.assertEqual(camera.motion, {})
if name in test_image:
self.assertEqual(camera.motion['image'], test_image[name])
else:
self.assertEqual(camera.motion, {})
if name in test_video:
self.assertEqual(camera.motion['time'], test_time[name])
else:
self.assertEqual(camera.motion, {})
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
def test_take_new_picture(self, mock_get, mock_post):
"""Checks if we can take a new picture and retrieve the thumbnail."""
self.blink.setup_system()
test_cameras = mresp.get_test_cameras(self.test_urls.base_url)
test_thumbnail = self.test_urls.base_url + mresp.NEW_THUMBNAIL + '.jpg'
# Snap picture for each camera and check new thumb
for camera_name in test_cameras:
camera = self.blink.cameras[camera_name]
camera.snap_picture()
camera.image_refresh()
self.assertEqual(camera.thumbnail, test_thumbnail)
# Manually set thumbnail, and then globally refresh and check
for camera_name in test_cameras:
camera = self.blink.cameras[camera_name]
camera.thumbnail = 'Testing'
self.assertEqual(camera.thumbnail, 'Testing')
self.blink.refresh()
for camera_name in test_cameras:
camera = self.blink.cameras[camera_name]
self.assertEqual(camera.thumbnail, test_thumbnail)
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
def test_image_with_bad_data(self, mock_get, mock_post):
"""Checks for handling of bad keys."""
self.blink.setup_system()
for camera_name in self.blink.cameras:
camera = self.blink.cameras[camera_name]
camera.snap_picture()
camera.urls.home_url = "use_bad_response"
self.assertEqual(camera.image_refresh(), None)
def test_camera_update(self):
"""Checks that the update function is doing the right thing."""
self.test_urls = blinkpy.BlinkURLHandler('test')
test_config = mresp.FIRST_CAMERA
self.urls = self.test_urls
test_camera = blinkpy.blinkpy.BlinkCamera(test_config, self)
test_update = mresp.SECOND_CAMERA
test_camera.update(test_update)
test_image_url = self.test_urls.base_url + test_update['thumbnail']
test_thumbnail = test_image_url + '.jpg'
test_clip = test_image_url + '.mp4'
self.assertEqual(test_camera.name, test_update['name'])
self.assertEqual(test_camera.armed, test_update['armed'])
self.assertEqual(test_camera.thumbnail, test_thumbnail)
self.assertEqual(test_camera.clip, test_clip)
self.assertEqual(test_camera.temperature, test_update['temp'])
self.assertEqual(test_camera.battery, test_update['battery'])
self.assertEqual(test_camera.notifications,
test_update['notifications'])
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
def test_camera_thumbs(self, mock_get, mock_post):
"""Checks to see if we can retrieve camera thumbs."""
self.test_urls = blinkpy.BlinkURLHandler(self.region_id)
test_cameras = mresp.get_test_cameras(self.test_urls.base_url)
self.blink.setup_system()
for name in self.blink.cameras:
thumb = self.blink.camera_thumbs[name]
self.assertEqual(test_cameras[name]['thumbnail'], thumb)
@mock.patch('blinkpy.blinkpy.copyfileobj',
side_effect=mresp.mocked_copyfileobj)
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
def test_image_to_file(self, mock_get, mock_post, mock_copyfileobj):
"""Checks that we can write an image to file."""
self.blink.setup_system()
cameras = self.blink.cameras
filename = '/tmp/test.jpg'
test_files = list()
for camera_name in cameras:
camera = cameras[camera_name]
test_files.append(mresp.MOCK_BYTES)
mock_fh = mock.mock_open()
with mock.patch('builtins.open', mock_fh, create=True):
camera.image_to_file(camera_name + filename)
mock_fh.assert_called_once_with(camera_name + filename, 'wb')
self.assertEqual(test_files, mresp.FAKE_FILES)
-100
View File
@@ -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
+201
View File
@@ -0,0 +1,201 @@
"""
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
import blinkpy
import tests.mock_responses as mresp
import helpers.constants as const
USERNAME = 'foobar'
PASSWORD = 'deadbeef'
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)
def tearDown(self):
"""Clean up after test."""
self.blink = None
self.blink_no_cred = None
def test_initialization(self):
"""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):
"""Check that we throw an exception when no username/password."""
with self.assertRaises(blinkpy.BlinkAuthenticationException):
self.blink_no_cred.get_auth_token()
with self.assertRaises(blinkpy.BlinkAuthenticationException):
self.blink_no_cred.setup_system()
# pylint: disable=protected-access
self.blink_no_cred._username = USERNAME
with self.assertRaises(blinkpy.BlinkAuthenticationException):
self.blink_no_cred.get_auth_token()
def test_no_auth_header(self):
"""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 = blinkpy.BlinkURLHandler(region_id)
with self.assertRaises(blinkpy.BlinkException):
self.blink.get_ids()
with self.assertRaises(blinkpy.BlinkException):
self.blink.get_summary()
@mock.patch('blinkpy.blinkpy.getpass.getpass')
def test_manual_login(self, getpwd):
"""Check that we can manually use the login() function."""
getpwd.return_value = PASSWORD
with mock.patch('builtins.input', return_value=USERNAME):
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)
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
def test_bad_request(self, mock_get, mock_post):
"""Check that we raise an Exception with a bad request."""
with self.assertRaises(blinkpy.BlinkException):
# pylint: disable=protected-access
blinkpy.blinkpy._request(None, reqtype='bad')
with self.assertRaises(blinkpy.BlinkAuthenticationException):
# pylint: disable=protected-access
blinkpy.blinkpy._request(None, reqtype='post', is_retry=True)
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
def test_system_bad_refresh(self, mock_get, mock_post):
"""Checks for handling of bad keys."""
self.blink.setup_system()
self.blink.urls.home_url = 'use_bad_response'
self.assertEqual(self.blink.refresh(), None)
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
def test_full_setup(self, mock_get, mock_post):
"""Check that we can set Blink up properly."""
self.blink.setup_system()
# Get all test values
authtoken = mresp.LOGIN_RESPONSE['authtoken']['authtoken']
(region_id, region), = mresp.LOGIN_RESPONSE['region'].items()
host = region_id + '.' + const.BLINK_URL
network_id = mresp.NETWORKS_RESPONSE['networks'][0]['id']
account_id = mresp.NETWORKS_RESPONSE['networks'][0]['account_id']
test_urls = blinkpy.BlinkURLHandler(region_id)
test_cameras = mresp.get_test_cameras(test_urls.base_url)
test_camera_id = mresp.get_test_id_table()
# Check that all links have been set properly
self.assertEqual(self.blink.region_id, region_id)
self.assertEqual(self.blink.urls.base_url, test_urls.base_url)
self.assertEqual(self.blink.urls.home_url, test_urls.home_url)
self.assertEqual(self.blink.urls.event_url, test_urls.event_url)
self.assertEqual(self.blink.urls.network_url, test_urls.network_url)
self.assertEqual(self.blink.urls.networks_url, test_urls.networks_url)
# Check that all properties have been set after startup
# pylint: disable=protected-access
self.assertEqual(self.blink._token, authtoken)
# pylint: disable=protected-access
self.assertEqual(self.blink._host, host)
self.assertEqual(self.blink.network_id, str(network_id))
self.assertEqual(self.blink.account_id, str(account_id))
self.assertEqual(self.blink.region, region)
# Verify we have initialized all the cameras
self.assertEqual(self.blink.id_table, test_camera_id)
for camera in test_cameras:
self.assertTrue(camera in self.blink.cameras)
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
def test_arm_disarm_system(self, mock_get, mock_post):
"""Check that we can arm/disarm the system"""
self.blink.setup_system()
self.blink.arm = False
self.assertIs(self.blink.arm, False)
self.blink.arm = True
self.assertIs(self.blink.arm, True)
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
def test_check_online_status(self, mock_get, mock_post):
"""Check that we can get our online status."""
self.blink.setup_system()
expected_status = const.ONLINE[mresp.RESPONSE['syncmodule']['status']]
self.assertIs(self.blink.online, expected_status)
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
def test_setup_backup_subdomain(self, mock_get, mock_post):
"""Check that we can use the 'rest.piri' subdomain."""
test_urls = blinkpy.BlinkURLHandler('rest.piri')
with mock.patch('helpers.constants.LOGIN_URL',
return_value=const.LOGIN_URL + 'NO'):
self.blink.setup_system()
self.assertEqual(self.blink.region_id, 'rest.piri')
# pylint: disable=protected-access
self.assertEqual(self.blink._host, 'rest.piri.' + const.BLINK_URL)
self.assertEqual(self.blink.urls.base_url, test_urls.base_url)
self.assertEqual(self.blink.urls.home_url, test_urls.home_url)
self.assertEqual(self.blink.urls.event_url, test_urls.event_url)
self.assertEqual(self.blink.urls.network_url, test_urls.network_url)
self.assertEqual(self.blink.urls.networks_url, test_urls.networks_url)
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
@mock.patch('blinkpy.blinkpy.BlinkURLHandler',
side_effect=mresp.MockURLHandler)
def test_continuous_bad_auth(self, mock_url, mock_get, mock_post):
"""Check that we don't get stuck if we can't reauthorize."""
with self.assertRaises(blinkpy.BlinkAuthenticationException):
self.blink.setup_system()
@mock.patch('blinkpy.blinkpy.requests.post',
side_effect=mresp.mocked_requests_post)
@mock.patch('blinkpy.blinkpy.requests.get',
side_effect=mresp.mocked_requests_get)
def test_reauthorization_attempt(self, mock_get, mock_post):
"""Check that we can reauthorize after first unsuccessful attempt."""
self.blink.setup_system()
# pylint: disable=protected-access
original_header = self.blink._auth_header
bad_header = {'Host': self.blink._host, 'TOKEN_AUTH': 'BADTOKEN'}
self.blink._auth_header = bad_header
# pylint: disable=protected-access
self.assertEqual(self.blink._auth_header, bad_header)
self.blink.get_summary()
# pylint: disable=protected-access
self.assertEqual(self.blink._auth_header, original_header)
-75
View File
@@ -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)
-157
View File
@@ -1,157 +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
SYNC_ID = 1000
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': SYNC_ID,
'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'},
'event': [{'camera_name': CAMERA_NAME,
'updated_at': '2017-01-28T19:51:52+00:00',
'sync_module_id': None,
'camera': DEVICE_ID,
'type': 'motion',
'duration': None,
'status': None,
'created_at': '2017-01-28T19:51:52+00:00',
'camera_id': DEVICE_ID,
'id': 123456789,
'siren_id': None,
'account_id': ACCOUNT_ID,
'notified': True,
'siren': None,
'syncmodule': None,
'video_url': THUMB+'.mp4',
'command_id': None,
'network_id': None,
'account': ACCOUNT_ID,
'video_id': 100000001},
{'updated_at': '2017-01-28T19:51:29+00:00',
'sync_module_id': SYNC_ID,
'camera': None,
'type': 'armed',
'duration': None,
'status': None,
'created_at':
'2017-01-28T19:51:29+00:00',
'camera_id': None,
'id': 123456789,
'siren_id': None,
'account_id': ACCOUNT_ID,
'notified': False,
'siren': None,
'syncmodule': SYNC_ID,
'command_id': None,
'network_id': None,
'account': ACCOUNT_ID},
{'updated_at': '2017-01-28T19:00:12+00:00',
'sync_module_id': SYNC_ID,
'camera': None,
'type': 'disarmed',
'duration': None,
'status': None,
'created_at': '2017-01-28T19:00:12+00:00',
'camera_id': None,
'id': 123456789,
'siren_id': None,
'account_id': 2463,
'notified': False,
'siren': None,
'syncmodule': SYNC_ID,
'command_id': None,
'network_id': None,
'account': ACCOUNT_ID},
{'camera_name': CAMERA_NAME,
'updated_at': '2017-01-28T18:59:55+00:00',
'sync_module_id': None,
'camera': DEVICE_ID,
'type': 'motion',
'duration': None,
'status': None,
'created_at': '2017-01-28T18:59:55+00:00',
'camera_id': DEVICE_ID,
'id': 123456789,
'siren_id': None,
'account_id': ACCOUNT_ID,
'notified': True,
'siren': None,
'syncmodule': None,
'command_id': None,
'network_id': None,
'account': ACCOUNT_ID}],
'syncmodule':{'name':'SyncName',
'status':ONLINE}}
-6
View File
@@ -1,6 +0,0 @@
import testtools
import blinkpy
class TestImport(testtools.TestCase):
def test_import(self):
pass
+11 -10
View File
@@ -1,23 +1,24 @@
[tox]
envlist = py34, pep8
envlist = py34, py35, py36, lint
skip_missing_interpreters = 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.py tests
flake8 blinkpy.py tests
pydocstyle blinkpy.py tests