Compare commits

..
152 Commits
Author SHA1 Message Date
Kevin FronczakandGitHub afdd73118d Merge pull request #165 from fronzbot/dev
0.13.1
2019-03-01 21:59:29 -05:00
Kevin FronczakandGitHub 5511af6244 Version bump 2019-03-01 21:51:03 -05:00
Kevin FronczakandGitHub d629d9fa3a Update CHANGES.rst 2019-03-01 21:50:36 -05:00
Kevin FronczakandGitHub 3e22d83962 Merge pull request #164 from fronzbot/throttle-hotfix
Remove throttle from network_status
2019-03-01 21:45:59 -05:00
Kevin Fronczak 57b05daad7 Remove throttle from network_status 2019-03-01 21:40:27 -05:00
Kevin FronczakandGitHub 419eb51b66 Dev version bump 2019-03-01 21:06:12 -05:00
Kevin FronczakandGitHub 0262f14a53 Merge pull request #163 from fronzbot/dev
0.13.0
2019-03-01 21:04:36 -05:00
Kevin FronczakandGitHub c4b255d3bf Version bump 2019-03-01 20:54:52 -05:00
Kevin FronczakandGitHub 13b7debb33 Update CHANGES.rst 2019-03-01 20:54:21 -05:00
Kevin FronczakandGitHub a203c4a29f Merge pull request #162 from fronzbot/new-login-api
Added ability to have multiple backup api endpoints for logging in
2019-03-01 20:34:03 -05:00
Kevin Fronczak d8bac39c52 Added ability to have multiple backup api endpoints for logging in 2019-03-01 16:03:22 -05:00
Kevin FronczakandGitHub f1c06198b2 Merge pull request #161 from fronzbot/throttle-refresh
Throttle refresh
2019-03-01 15:22:30 -05:00
Kevin Fronczak 95b45f507a Change api throttle time to 2 seconds 2019-03-01 15:11:23 -05:00
Kevin Fronczak 54bfb99d60 Added simple throttle for refresh to prevent too many force calls 2019-03-01 15:10:28 -05:00
Kevin FronczakandGitHub 9dda477e3d Merge pull request #160 from fronzbot/api-change
Api change
2019-03-01 14:55:38 -05:00
Kevin Fronczak 2ca6b9edda -_- 2019-03-01 14:25:46 -05:00
Kevin Fronczak b22764614d Upgrade pylint to 2.3.0 2019-03-01 14:08:22 -05:00
Kevin Fronczak 09c168f6e8 Remove blank line at EOF 2019-03-01 13:53:13 -05:00
Kevin Fronczak 414eabd3ae Add test for get_camera method. Downgrade travis lint to 3.5.3 2019-03-01 13:52:13 -05:00
Kevin Fronczak b536bd7eb5 Build back to 3.6, forgot dist for lint 3.7 2019-03-01 13:40:10 -05:00
Kevin Fronczak 532a8d5071 lint and build python version now 3.7 2019-03-01 13:36:19 -05:00
Kevin Fronczak 793797e3a7 Fixed issues with new endpoint 2019-03-01 13:09:03 -05:00
Kevin Fronczak 602b6160ab New method to retrieve cameras due to api change 2019-03-01 11:35:30 -05:00
Kevin FronczakandGitHub aff95ba9e9 Merge pull request #158 from fronzbot/throttle-api-calls
Added throttles to api calls
2019-02-26 10:06:49 -05:00
Kevin Fronczak 74aad2af1c Added throttles to api calls 2019-02-26 09:53:49 -05:00
Kevin FronczakandGitHub b96ad8b4e4 Merge pull request #157 from fronzbot/throttle-decorator
Adds throttle decorator
2019-02-17 20:27:15 -08:00
Kevin Fronczak a8b731fe21 Adds throttle decorator 2019-02-17 23:19:28 -05:00
Kevin FronczakandGitHub ff8f091a74 Merge pull request #154 from fronzbot/dev
0.12.1
2019-01-31 18:22:09 -08:00
Kevin Fronczak 147b6143b0 Dev version bump 2019-01-31 21:21:17 -05:00
Kevin FronczakandGitHub 2835ccf7d2 Version bump 2019-01-31 21:18:02 -05:00
Kevin FronczakandGitHub 4deb53eab8 Update CHANGES.rst 2019-01-31 21:17:30 -05:00
Kevin FronczakandGitHub 1c49b1a943 Merge pull request #153 from fronzbot/revert-logging
Remove logging improvements because they sucked
2019-01-31 18:16:10 -08:00
Kevin Fronczak 67cd4ab962 fix lint 2019-01-31 21:08:55 -05:00
Kevin Fronczak 1a2017b337 Remove logging improvements because they sucked 2019-01-31 21:02:06 -05:00
Kevin Fronczak 7775898078 Update Changes 2019-01-31 20:43:56 -05:00
Kevin FronczakandGitHub 8f0bd1224d Dev version bump 2019-01-31 20:06:30 -05:00
Kevin FronczakandGitHub 77e200f6a5 Merge pull request #152 from fronzbot/dev
0.12.0
2019-01-31 17:05:03 -08:00
Kevin Fronczak d880998c43 Version bump 2019-01-31 19:58:15 -05:00
Kevin FronczakandGitHub fbd82030bb Merge pull request #151 from fronzbot/fix-log-format
Added log formatter
2019-01-31 16:42:47 -08:00
Kevin Fronczak 162b409c52 Added log formatter 2019-01-31 19:33:29 -05:00
Kevin FronczakandGitHub 8e2d2e81e8 Merge pull request #150 from fronzbot/fix-logging
Add improved logging functionality.
2019-01-31 11:10:15 -08:00
Kevin FronczakandGitHub 5504dfa94e Merge branch 'dev' into fix-logging 2019-01-31 10:58:56 -08:00
Kevin FronczakandGitHub d5f866bd34 Merge pull request #149 from fronzbot/add-download-videos
Add download videos routine
2019-01-31 10:56:08 -08:00
Kevin FronczakandGitHub 02937c0ef5 Added more info to README 2019-01-31 13:41:26 -05:00
Kevin Fronczak ae5d73e197 Fix for lint errors 2019-01-31 13:24:14 -05:00
Kevin Fronczak 15da35a6bc Add improved logging functionality. 2019-01-31 13:18:53 -05:00
Kevin Fronczak 9738f84457 Add download videos routine 2019-01-31 12:56:08 -05:00
Kevin FronczakandGitHub 663a128469 Merge pull request #148 from fronzbot/backup-temp
Set calibrated temperature to default to temperature if endpoint fails
2019-01-30 13:24:21 -08:00
Kevin FronczakandGitHub bc14ab053c Merge branch 'dev' into backup-temp 2019-01-30 13:01:53 -08:00
Kevin FronczakandGitHub 3b897f6dbc Merge pull request #147 from fronzbot/better-logging
Add repeat log handling filter.
2019-01-29 20:34:08 -08:00
Kevin Fronczak 92f908928d Set calibrated temperature to default to temperature if endpoint fails 2019-01-29 23:13:31 -05:00
Kevin Fronczak da537773a7 Add repeat log handling filter.
- Added exc_info=True to some errors for traceback logging
2019-01-29 23:09:28 -05:00
Kevin FronczakandGitHub 778592c6cf Merge pull request #146 from fronzbot/typo
Fix typo in video url
2019-01-28 11:15:52 -08:00
Kevin Fronczak 0e33457ed0 Fix typo in video url 2019-01-28 14:10:13 -05:00
Kevin FronczakandGitHub abcf2ae1a1 Merge pull request #145 from fronzbot/dont-poop-on-failures
Dont poop on failures
2019-01-26 18:06:50 -08:00
Kevin Fronczak 28fb53029c Fixed recurssion error by adding is_retry property to get_auth_token 2019-01-26 20:56:10 -05:00
Kevin Fronczak d809cbcb3d Stashing commit. Getting RecursonError on failed login. 2019-01-26 20:14:50 -05:00
Kevin Fronczak 8dea1a4312 Added try/excepts around methods that would bomb setup if api fails 2019-01-26 19:41:01 -05:00
Kevin FronczakandGitHub 45f7dd6cbb Merge pull request #144 from fronzbot/api-doc-strings
Api doc strings
2019-01-26 13:41:21 -08:00
Kevin Fronczak ac28dddd8b Finished adding docstrings, added version attr to Blink class 2019-01-26 16:20:48 -05:00
Kevin Fronczak aa58383318 Started adding doctrings. 2019-01-26 16:07:59 -05:00
Kevin FronczakandGitHub b0698f246a Merge pull request #143 from fronzbot/fix-video-api
Fix video api to re-enable motion detection
2019-01-26 12:55:32 -08:00
Kevin Fronczak 46c5d5eb3c Added check videos tests
- Moved motion detection setting to happen prior to return to prevent
motion detect sticking upon api failure
2019-01-26 15:40:02 -05:00
Kevin Fronczak 1922b2535c Fix linting errors 2019-01-26 14:45:11 -05:00
Kevin Fronczak 4297290fba Fix broken tests 2019-01-26 14:35:28 -05:00
Kevin Fronczak 1daf5d22af Fix lint, remove some camera tests 2019-01-25 20:51:14 -05:00
Kevin Fronczak 8fd989e991 Motion detection now working, removed get_videos method 2019-01-25 20:44:31 -05:00
Kevin Fronczak 559cc4a5ac First cut at fixing video api 2019-01-25 13:51:07 -05:00
Kevin FronczakandGitHub 66a66c9bd3 Dev version bump 2019-01-23 11:05:34 -05:00
Kevin FronczakandGitHub f3a8036649 Merge pull request #141 from fronzbot/dev
0.11.2
2019-01-23 08:05:05 -08:00
Kevin FronczakandGitHub 5c6e4364e8 Update CHANGES.rst 2019-01-23 10:57:18 -05:00
Kevin FronczakandGitHub fd64f41b22 Update constants.py 2019-01-23 10:55:39 -05:00
Kevin FronczakandGitHub 0d7aac3458 Merge pull request #140 from fronzbot/hotfix
Added some error handling for video method due to api changes
2019-01-23 07:54:19 -08:00
Kevin Fronczak 0bebcd8303 Added some error handling for video method due to api changes 2019-01-23 10:48:51 -05:00
Kevin Fronczak ff35c56c9a Dev version bump 2019-01-02 12:24:30 -05:00
Kevin FronczakandGitHub 853ccbce79 Merge pull request #136 from fronzbot/dev
0.11.1
2019-01-02 09:19:08 -08:00
Kevin Fronczak 8153fab887 Version bump 2019-01-02 11:59:27 -05:00
Kevin FronczakandGitHub 216541288e Merge pull request #135 from fronzbot/link-fix
Fixed link formatting
2019-01-02 08:53:08 -08:00
Kevin FronczakandGitHub a855cceb05 Merge pull request #134 from fronzbot/calibrated-temperature
Add calibrated temperature property (fixes #133)
2019-01-02 08:52:56 -08:00
Kevin Fronczak d9991d5acd Fixed link formatting (fixes #132) 2019-01-02 11:45:48 -05:00
Kevin Fronczak 2ee2ae7d4e Add calibrated temperature property (fixes #133) 2019-01-02 11:35:40 -05:00
Kevin FronczakandGitHub 04c2be9087 Add changes from source repo 2018-11-29 15:23:28 -05:00
Kevin Fronczak 34fc9bc527 dev version bump 2018-11-23 20:32:12 -05:00
Kevin FronczakandGitHub 1a24e1896f Merge pull request #129 from fronzbot/dev
0.11.0
2018-11-23 20:27:40 -05:00
Kevin Fronczak 1d67764dc8 version bump 2018-11-23 20:09:46 -05:00
Kevin Fronczak 8f250d204f Fixed docs for real 2018-11-23 20:08:21 -05:00
Kevin Fronczak 643ae8c387 trying again 2018-11-23 19:44:25 -05:00
Kevin Fronczak f9615a2d90 fix docs again 2018-11-23 19:39:23 -05:00
Kevin Fronczak f7a3b1d0ef Update docs 2018-11-23 19:37:38 -05:00
Kevin FronczakandGitHub 94aed43d2e Merge pull request #128 from fronzbot/fix-sync-refresh
Fix broken sync refresh
2018-11-23 19:25:39 -05:00
Kevin Fronczak 90f30deea8 Fix broken sync refresh 2018-11-23 19:20:18 -05:00
Kevin FronczakandGitHub 1bbc71ef17 Merge pull request #125 from fronzbot/multi-sync-support
Support for Multiple Sync Modules
2018-11-23 18:23:03 -05:00
Kevin Fronczak c84a0deb5f Add sync parent to attributes 2018-11-23 18:08:58 -05:00
Kevin Fronczak ece65a9158 Fix linting errors 2018-11-23 17:52:12 -05:00
Kevin Fronczak 5a8af75a5b Added cameras dict to blink to keep track of ALL cameras 2018-11-23 17:47:44 -05:00
Kevin Fronczak 84f987d29c Updated to reflect multi-sync unit support 2018-11-23 16:46:20 -05:00
Kevin Fronczak ed25745deb Added case insesnitive dict, changed sync module name to network name
(to mimic app)
2018-11-23 16:36:17 -05:00
Kevin Fronczak a1fd05735e Initial support for multiple sync modules 2018-11-23 14:22:54 -05:00
Kevin FronczakandGitHub 5a9f8f6dfc Merge pull request #124 from fronzbot/improve-contribution-doc
Improve contribution instructions
2018-11-21 10:08:31 -05:00
Kevin FronczakandGitHub 2639055372 Merge pull request #123 from fronzbot/update-readme
Update README.rst
2018-11-21 10:03:18 -05:00
Kevin FronczakandGitHub c53b2bd777 Improve contribution instructions 2018-11-21 10:02:12 -05:00
Kevin FronczakandGitHub db29bae140 Broke out blink home assistant links 2018-11-21 09:38:23 -05:00
Kevin FronczakandGitHub 52f1fde47a Bad link format 2018-11-21 09:37:27 -05:00
Kevin FronczakandGitHub e705fbd403 Update README.rst 2018-11-21 09:36:36 -05:00
Kevin FronczakandGitHub 2d437b4462 Merge pull request #122 from fronzbot/dev-bump
Dev version bump
2018-11-20 21:53:09 -05:00
Kevin FronczakandGitHub ae946dbb53 Merge pull request #121 from fronzbot/dev
0.10.3
2018-11-20 21:47:42 -05:00
Kevin Fronczak ea2b22dbf0 Dev version bump 2018-11-20 21:46:38 -05:00
Kevin Fronczak 7ee57b2c6b Version bump 2018-11-20 21:40:18 -05:00
Kevin FronczakandGitHub ccd393a533 Merge pull request #120 from fronzbot/fix-bad-logging
Logging fixes
2018-11-20 21:26:32 -05:00
Kevin Fronczak 1b571c7d4b Fix logging string format 2018-11-20 21:21:42 -05:00
Kevin Fronczak 4b91fcb615 Improved video page debug logging 2018-11-20 21:20:35 -05:00
Kevin Fronczak 1e0d069dcd Fix linting errors 2018-11-20 21:16:41 -05:00
Kevin Fronczak 92088779e0 Added debug logging 2018-11-20 21:15:01 -05:00
Kevin Fronczak a6e02433b6 Fix #113 2018-11-20 21:12:12 -05:00
Kevin FronczakandGitHub 1aa5dc1c71 Merge pull request #119 from md-reddevil/dev
Use network_status instead of homescreen
2018-11-19 12:49:47 -05:00
Kevin Fronczak 3c78923853 Fix linting error (line was too long) 2018-11-19 12:46:01 -05:00
Kevin Fronczak d17183a152 Fixed broken sync module test 2018-11-19 12:42:54 -05:00
2e5e343913 Update blinkpy/sync_module.py fix linting
Co-Authored-By: md-reddevil <16707502+md-reddevil@users.noreply.github.com>
2018-11-18 14:06:21 +02:00
md-reddevilandGitHub cec6035d23 fix tests after API changes 2018-11-18 14:01:22 +02:00
md-reddevil bdc760608e fix indentation 2018-11-17 18:20:28 +02:00
md-reddevil 4f41899338 fix indentation 2018-11-17 18:18:57 +02:00
md-reddevil 557f21b637 Use network_status instead of homescreen
Use network_status to check if System is armed or disarmed
2018-11-17 18:12:26 +02:00
Kevin FronczakandGitHub 4241b50002 Merge pull request #118 from md-reddevil/dev
API: Fix Command Status method add a new method to manage correctly the System status
2018-11-14 09:01:10 -05:00
md-reddevilandGitHub 8b0507f1a7 fix indentation for method request_command_status 2018-11-14 15:57:46 +02:00
md-reddevilandGitHub d62429189e Fix formatting issue in api.py 2018-11-14 11:04:40 +02:00
md-reddevilandGitHub c83b173a17 update description of the networks method 2018-11-14 09:55:17 +02:00
md-reddevilandGitHub 9c1974c227 Add method to request the Network/System status
Home screen method returns just the information about last added System(network). The method "request_network_status", should replace the homescreen method in order to obtain all the information about a system.
2018-11-14 09:39:44 +02:00
md-reddevilandGitHub c621ba3150 fix check command status 2018-11-14 09:34:25 +02:00
Kevin FronczakandGitHub 7710e3f972 Errant whitespace 2018-10-30 21:08:43 -04:00
Kevin Fronczak a4f65c1102 dev version bump 2018-10-30 20:24:39 -04:00
Kevin FronczakandGitHub 80c6444c2e Merge pull request #112 from fronzbot/dev
0.10.2
2018-10-30 20:22:08 -04:00
Kevin Fronczak 2022b80f9d Version bump 2018-10-30 20:16:37 -04:00
Kevin FronczakandGitHub 7e9900c5f5 Merge pull request #111 from fronzbot/fix-typo
Reformatted log message when multiple networks detected.
2018-10-30 20:07:03 -04:00
Kevin Fronczak 132df9e8c4 Fixed wrong string format 2018-10-30 19:58:13 -04:00
Kevin Fronczak 07e3549306 Reformatted log message when multiple networks detected. 2018-10-30 19:48:08 -04:00
Kevin FronczakandGitHub e0fa0a0cde Merge pull request #110 from fronzbot/upgrade-requests
Upgraded requests due to security issue prior to 2.20.0
2018-10-30 19:47:38 -04:00
Kevin Fronczak 31771ca87d Upgraded requests due to security issue prior to 2.20.0 2018-10-30 19:42:35 -04:00
Kevin FronczakandGitHub 1b898398a7 Fix rst lint error 2018-10-26 11:09:43 -04:00
Kevin FronczakandGitHub d8fcb679cd Add python versioning badge 2018-10-26 11:05:00 -04:00
Kevin Fronczak 72a5ccb769 dev version bump 2018-10-18 13:20:34 -04:00
Kevin FronczakandGitHub e872fc9ce5 Merge pull request #105 from fronzbot/dev
0.10.1
2018-10-18 13:19:49 -04:00
Kevin Fronczak 51961474b6 Version bump 2018-10-18 13:10:42 -04:00
Kevin FronczakandGitHub 8be35dab30 Merge pull request #104 from fronzbot/fix-warning
Changed 'warning' to 'info' in camera
2018-10-18 13:05:17 -04:00
Kevin FronczakandGitHub 0ab17aede8 Merge pull request #103 from fronzbot/reauth-bug-fix
Reauth bug fix
2018-10-18 13:01:52 -04:00
Kevin Fronczak 6db8a151f8 Changed 'warning' to 'info' and added an error if trying to save video
that doesn't exist
2018-10-18 12:58:43 -04:00
Kevin Fronczak 1a12eb367f Added additional re-auth attempt in try/except 2018-10-18 12:49:18 -04:00
Kevin Fronczak f61f68a5c2 Fix reauthorization bug 2018-10-18 12:23:23 -04:00
Kevin FronczakandGitHub 0781d79ca7 Update bug_report.md 2018-10-17 11:18:29 -04:00
Kevin FronczakandGitHub 4ffdf499c9 Update PULL_REQUEST_TEMPLATE.md 2018-10-17 10:28:40 -04:00
Kevin FronczakandGitHub f8f318c203 Update bug_report.md 2018-10-17 10:26:28 -04:00
Kevin Fronczak 59540818f9 Add feature request template 2018-10-17 10:24:52 -04:00
Kevin Fronczak b2b531ad3c Add issue template 2018-10-17 10:22:59 -04:00
27 changed files with 1258 additions and 474 deletions
+29
View File
@@ -0,0 +1,29 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1.
2.
3.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Home Assistant version (if applicable):** <HA Version Number>
**`blinkpy` version (not needed if filling out Home Assistant version):**
**Log Output/Additional Information**
If using home-assistant, please paste the output of the log showing your error below. If not, please include any additional useful information.
```
PASTE LOG OUTPUT HERE
```
+14
View File
@@ -0,0 +1,14 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context about the feature request (such as API endpoint responses, etc).
+2 -2
View File
@@ -5,5 +5,5 @@
## Checklist:
- [ ] Local tests with `tox` run successfully **PR cannot be meged unless tests pass**
- [ ] If user-facing functionality changed, README.rst updated
- [ ] Tests added to verify new code works
- [ ] Changes tested locally to ensure platform still works as intended
- [ ] Tests added to verify new code works
+1
View File
@@ -10,3 +10,4 @@ dist/*
.sh
build/*
docs/_build
*.log
+1 -1
View File
@@ -2,7 +2,7 @@ sudo: required
matrix:
fast_finish: true
include:
- python: "3.6"
- python: "3.5.3"
env: TOXENV=lint
- python: "3.5.3"
env: TOXENV=py35
+36 -34
View File
@@ -1,5 +1,5 @@
# BlinkMonitorProtocol
Unofficial documentation for the Client API of the Blink Wire-Free HD Home Monitoring and Alert System.
Unofficial documentation for the Client API of the Blink Wire-Free HD Home Monitoring &amp; Alert System.
Copied from https://github.com/MattTW/BlinkMonitorProtocol
@@ -7,9 +7,6 @@ I am not affiliated with the company in any way - this documentation is strictly
The Client API is a straightforward REST API using JSON and HTTPS.
## This Document
The purpose here is to describe what is going on behind the scenes, and what commands we know are available to communicate with Blink servers. This is NOT a description of the blinkpy module, but a description of the commands blinkpy relies on to effectively communicate.
## Login
Client login to the Blink Servers.
@@ -19,20 +16,25 @@ Client login to the Blink Servers.
> "password" : "*your blink password*",
> "client_specifier" : "iPhone 9.2 | 2.2 | 222",
> "email" : "*your blink login/email*"
>}' --compressed https://prod.immedia-semi.com/login
>}' --compressed https://rest.prod.immedia-semi.com/login
**Response:**
>{"authtoken":{"authtoken":"*an auth token*","message":"auth"}}
>{"authtoken":{"authtoken":"*an auth token*","message":"auth"},"networks":{"*network id*":{"name":"*name*","onboarded":true}},"region":{"*regioncode for endpoint*":"*region name"}}
**Notes:**
The authtoken value is passed in a header in future calls.
The region code for endpoint is required to form the URL of the REST endpoint for future calls.
Depending on the region you are registered you will need to change the REST endpoints below:
- from `https://rest.prod.immedia-semi.com`
- to `https://rest.prde.immedia-semi.com` if e.g. your device is registered in Germany
Please note that at this moment it seems that all regions are not implemented equally: not all endpoints are available in all regions
##Networks
## Networks
Obtain information about the Blink networks defined for the logged in user.
**Request:**
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/networks
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/networks
**Response:**
JSON response containing information including Network ID and Account ID.
@@ -46,7 +48,7 @@ Network ID is needed to issue arm/disarm calls
Obtain information about the Blink Sync Modules on the given network.
**Request:**
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id_from_networks_call*/syncmodules
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id_from_networks_call*/syncmodules
**Response:**
JSON response containing information about the known state of the Sync module, most notably if it is online
@@ -56,11 +58,11 @@ Probably not strictly needed but checking result can verify that the sync module
## Arm
Arm the given network (start recording/reporting motion events)
**Request:**
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id_from_networks_call*/arm
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id_from_networks_call*/arm
**Response:**
JSON response containing information about the arm command request, including the command/request ID
@@ -73,7 +75,7 @@ When this call returns, it does not mean the arm request is complete, the clien
Disarm the given network (stop recording/reporting motion events)
**Request:**
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id_from_networks_call*/disarm
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id_from_networks_call*/disarm
**Response:**
JSON response containing information about the disarm command request, including the command/request ID
@@ -87,7 +89,7 @@ When this call returns, it does not mean the disarm request is complete, the cl
Get status info on the given command
**Request:**
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/command/*command_id*
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/command/*command_id*
**Response:**
JSON response containing state information of the given command, most notably whether it has completed and was successful.
@@ -103,7 +105,7 @@ lv_relay, arm, disarm, thumbnail, clip
Return information displayed on the home screen of the mobile client
**Request:**
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/homescreen
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/homescreen
**Response:**
JSON response containing information that the mobile client displays on the home page, including: status, armed state, links to thumbnails for each camera, etc.
@@ -111,12 +113,12 @@ JSON response containing information that the mobile client displays on the home
**Notes:**
Not necessary to as part of issuing arm/disarm commands, but contains good summary info.
##Events, thumbnails & video captures
## Events, thumbnails & video captures
**Request**
Get events for a given network (sync module) -- Need network ID from home
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/events/network/*network__id*
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/events/network/*network__id*
**Response**
A json list of evets incluing URL's. Replace the "mp4" with "jpg" extension to get the thumbnail of each clip
@@ -144,7 +146,7 @@ Note that you replace the 'mp4' with a 'jpg' to get the thumbnail
**Request**
Captures a new thumbnail for a camera
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/thumbnail
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/thumbnail
**Response**
Command information.
@@ -152,17 +154,17 @@ Command information.
**Request**
Captures a new video for a camera
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/clip
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/clip
**Response**
Command information.
##Video Information
## Video Information
**Request**
Get the total number of videos in the system
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/videos/count
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/videos/count
**Response**
JSON response containing the total video count.
@@ -170,7 +172,7 @@ JSON response containing the total video count.
**Request**
Gets a paginated set of video information
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/videos/page/0
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/videos/page/0
**Response**
JSON response containing a set of video information, including: camera name, creation time, thumbnail URI, size, length
@@ -178,7 +180,7 @@ JSON response containing a set of video information, including: camera name, cre
**Request**
Gets information for a specific video by ID
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/video/*video_id*
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/video/*video_id*
**Response**
JSON response containing video information
@@ -186,7 +188,7 @@ JSON response containing video information
**Request**
Gets a list of unwatched videos
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/videos/unwatched
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/videos/unwatched
**Response**
JSON response containing unwatched video information
@@ -194,7 +196,7 @@ JSON response containing unwatched video information
**Request**
Deletes a video
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/api/v2/video/*video_id*/delete
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/api/v2/video/*video_id*/delete
**Response**
Unknown - not tested
@@ -202,7 +204,7 @@ Unknown - not tested
**Request**
Deletes all videos
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/api/v2/videos/deleteall
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/api/v2/videos/deleteall
**Response**
Unknown - not tested
@@ -212,7 +214,7 @@ Unknown - not tested
**Request**
Gets a list of cameras
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/cameras
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/cameras
**Response**
JSON response containing camera information
@@ -220,7 +222,7 @@ JSON response containing camera information
**Request**
Gets information for one camera
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*
**Response**
JSON response containing camera information
@@ -228,7 +230,7 @@ JSON response containing camera information
**Request**
Gets camera sensor information
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/signals
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/signals
**Response**
JSON response containing camera sensor information, such as wifi strength, temperature, and battery level
@@ -236,7 +238,7 @@ JSON response containing camera sensor information, such as wifi strength, tempe
**Request**
Enables motion detection for one camera
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/enable
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/enable
**Response**
JSON response containing camera information
@@ -244,7 +246,7 @@ JSON response containing camera information
**Request**
Disables motion detection for one camera
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/disable
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/disable
**Response**
JSON response containing camera information
@@ -257,7 +259,7 @@ JSON response containing camera information
**Request**
Gets information about devices that have connected to the blink service
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/account/clients
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/account/clients
**Response**
JSON response containing client information, including: type, name, connection time, user ID
@@ -265,7 +267,7 @@ JSON response containing client information, including: type, name, connection t
**Request**
Gets information about supported regions
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/regions
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/regions
**Response**
JSON response containing region information
@@ -273,7 +275,7 @@ JSON response containing region information
**Request**
Gets information about system health
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/health
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/health
**Response**
"all ports tested are open"
@@ -281,7 +283,7 @@ Gets information about system health
**Request**
Gets information about programs
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v1/networks/*network_id*/programs
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v1/networks/*network_id*/programs
**Response**
Unknown.
+84 -9
View File
@@ -3,16 +3,91 @@ Changelog
A list of changes between each release
0.13.1 (2019-03-01)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Remove throttle decorator from network status request
0.13.0 (2019-03-01)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**Breaking change:**
Wifi status reported in dBm again, instead of bars (which is great). Also, the old `get_camera_info` method has changed and requires a `camera_id` parameter.
- Adds throttle decorator
- Decorate following functions with 4s throttle (call method with `force=True` to override):
- request_network_status
- request_syncmodule
- request_system_arm
- request_system_disarm
- request_sync_events
- request_new_image
- request_new_video
- request_video_count
- request_cameras
- request_camera_info
- request_camera_sensors
- request_motion_detection_enable
- request_motion_detection_disable
- Use the updated homescreen api endpoint to retrieve camera information. The old method to retrieve all cameras at once seems to not exist, and this was the only solution I could figure out and confirm to work.
- Adds throttle decorator to refresh function to prevent too many frequent calls with `force_cache` flag set to `True`. This additional throttle can be overridden with the `force=True` argument passed to the refresh function.
- Add ability to cycle through login api endpoints to anticipate future endpoint deprecation
0.12.1 (2019-01-31)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Remove logging improvements since they were incompatible with home-assistant logging
0.12.0 (2019-01-31)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Fix video api endpoint, re-enables motion detection
- Add improved logging capability
- Add download video method
- Prevent blinkpy from failing at setup due to api error
0.11.2 (2019-01-23)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Hotfix to prevent platform from stalling due to API change
- Motion detection and video recovery broken until new API endpoint discovered
0.11.1 (2019-01-02)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Fixed incorrect backup login url
- Added calibrated temperature property for cameras
0.11.0 (2018-11-23)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Added support for multiple sync modules
0.10.3 (2018-11-18)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Use networks endpoint rather than homecreen to retrieve arm/disarm status (`@md-reddevil <https://github.com/fronzbot/blinkpy/pull/119>`__)
- Fix incorrect command status endpoint (`@md-reddevil <https://github.com/fronzbot/blinkpy/pull/118>`__)
- Add extra debug logging
- Remove error prior to re-authorization (only log error when re-auth failed)
0.10.2 (2018-10-30)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Set minimum required version of the requests library to 2.20.0 due to vulnerability in earlier releases.
- When multiple networks detected, changed log level to 'warning' from 'error'
0.10.1 (2018-10-18)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Fix re-authorization bug (fixes `#101 <https://github.com/fronzbot/blinkpy/issues/#101>`__)
- Log an error if saving video that doesn't exist
0.10.0 (2018-10-16)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Moved all API calls to own module for easier maintainability
- Added network ids to sync module and cameras to allow for multi-network use
- Removed dependency on video existance prior to camera setup (fixes `#93 <https://github.com/fronzbot/blinkpy/issues/#93>`_)
- Removed dependency on video existance prior to camera setup (fixes `#93 <https://github.com/fronzbot/blinkpy/issues/#93>`__)
- Camera wifi_strength now reported in wifi "bars" rather than dBm due to API endpoint change
- Use homescreen thumbnail as fallback in case it's not in the camera endpoint
- Removed "armed" and "status" attributes from camera (status of camera only reported by "motion_enabled" now)
- Added serial number attributes to sync module and cameras
- Check network_id from login response and verify that network is onboarded (fixes `#90 <https://github.com/fronzbot/#90>`_)
- Check network_id from login response and verify that network is onboarded (fixes `#90 <https://github.com/fronzbot/#90>`__)
- Check if retrieved clip is "None" prior to storing in cache
0.9.0 (2018-09-27)
@@ -44,26 +119,26 @@ A list of changes between each release
0.8.0 (2018-05-21)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Added support for battery voltage level (fixes `#64 <https://github.com/fronzbot/blinkpy/issues/64>`_)
- Added support for battery voltage level (fixes `#64 <https://github.com/fronzbot/blinkpy/issues/64>`__)
- Added motion detection per camera
- Added fully accessible camera configuration dict
- Added celcius property to camera (fixes `#60 <https://github.com/fronzbot/blinkpy/issues/60>`_)
- Added celcius property to camera (fixes `#60 <https://github.com/fronzbot/blinkpy/issues/60>`__)
0.7.1 (2018-05-09)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Fixed pip 10 import issue during setup (`@fronzbot <https://github.com/fronzbot/blinkpy/pull/61>`_)
- Fixed pip 10 import issue during setup (`@fronzbot <https://github.com/fronzbot/blinkpy/pull/61>`__)
0.7.0 (2018-02-08)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Fixed style errors for bumped pydocstring and pylint versions
- Changed Blink.cameras dictionary to be case-insensitive (fixes `#35 <https://github.com/fronzbot/blinkpy/issues/35>`_)
- Changed api endpoint for video extraction (fixes `#35 <https://github.com/fronzbot/blinkpy/issues/35>`_ and `#41 <https://github.com/fronzbot/blinkpy/issues/41>`_)
- Changed Blink.cameras dictionary to be case-insensitive (fixes `#35 <https://github.com/fronzbot/blinkpy/issues/35>`__)
- Changed api endpoint for video extraction (fixes `#35 <https://github.com/fronzbot/blinkpy/issues/35>`__ and `#41 <https://github.com/fronzbot/blinkpy/issues/41>`__)
- Removed last_motion() function from Blink class
- Refactored code for better organization
- Moved some request calls out of @property methods (enables future CLI support)
- Renamed get_summary() method to summary and changed to @property
- Added ability to download most recent video clip
- Improved camera arm/disarm handling (`@b10m <https://github.com/fronzbot/blinkpy/pull/50>`_)
- Improved camera arm/disarm handling (`@b10m <https://github.com/fronzbot/blinkpy/pull/50>`__)
- Added authentication to ``login()`` function and deprecated ``setup_system()`` in favor of ``start()``
- Added ``attributes`` dictionary to camera object
@@ -72,7 +147,7 @@ A list of changes between each release
- Removed redundent properties that only called hidden variables
- Revised request wrapper function to be more intelligent
- Added tests to ensure exceptions are caught and handled (100% coverage!)
- Added auto-reauthorization (token refresh) when a request fails due to an expired token (`@tySwift93 <https://github.com/fronzbot/blinkpy/pull/24>`_)
- Added auto-reauthorization (token refresh) when a request fails due to an expired token (`@tySwift93 <https://github.com/fronzbot/blinkpy/pull/24>`__)
- Added battery level string to reduce confusion with the way Blink reports battery level as integer from 0 to 3
0.5.2 (2017-03-12)
+41 -35
View File
@@ -4,41 +4,55 @@ Everyone is welcome to contribute to blinkpy! The process to get started is desc
## Fork the Repository
You can do this right in gituhb: just click the 'fork' button at the top right.
You can do this right in github: just click the 'fork' button at the top right.
## Setup Local Repository
## Start Developing
```shell
$ git clone https://github.com/<YOUR_GIT_USERNAME>/blinkpy.git
$ cd blinkpy
$ git remote add upstream https://github.com/fronzbot/blinkpy.git
```
1. Setup Local Repository
```shell
$ git clone https://github.com/<YOUR_GIT_USERNAME>/blinkpy.git
$ cd blinkpy
$ git remote add upstream https://github.com/fronzbot/blinkpy.git
```
## Create a Local Branch
2. Create a Local Branch
First, you will want to create a new branch to hold your changes:
``git checkout -b <your-branch-name>``
First, you will want to create a new branch to hold your changes:
``git checkout -b <your-branch-name>``
Next, you need to make sure you pull from the 'dev' branch:
``git pull origin dev``
## Make changes
3. Make changes
Now you can make changes to your code. It is worthwhile to test your code as you progress (see the **Testing** section)
Now you can make changes to your code. It is worthwhile to test your code as you progress (see the **Testing** section)
4. Commit Your Changes
To commit changes to your branch, simply add the files you want and the commit them to the branch. After that, you can push to your fork on GitHub:
```shell
$ git add .
$ git commit -m "Put your commit text here. Please be concise, but descriptive."
$ git push origin HEAD
```
5. Submit your pull request on GitHub
- On GitHub, navigate to the [blinkpy](https://github.com/fronzbot/blinkpy) repository.
- In the "Branch" menu, choose the branch that contains your commits (from your fork).
- To the right of the Branch menu, click New pull request.
- The base branch dropdown menu should read `dev`. Use the compare branch drop-down menu to choose the branch you made your changes in.
- Type a title and complete the provided description for your pull request.
- Click Create pull request.
- More detailed instructions can be found here: [Creating a Pull Request](https://help.github.com/articles/creating-a-pull-request/)
6. Prior to merge approval
Finally, the `blinkpy` repository uses continuous integration tools to run tests prior to merging. If there are any problems, you will see a red 'X' next to your pull request. To see what's wrong, you can find your pull request [here](https://travis- ci.org/fronzbot/blinkpy/pull_requests) and click on the failing test to see the logs. Those logs will indicate, as best as they can, what is causing that test to fail.
## Commit Your Changes
To commit changes to your branch, simply add the files you want and the commit them to the branch. After that, you can push to your fork on GitHub:
```shell
$ git add .
$ git commit -m "Put your commit text here. Please be concise, but descriptive."
$ git push origin HEAD
```
## Testing
It is important to test the code to make sure your changes don't break anything major and that they pass PEP8 style conventions.
FIrst, you need to locally install ``tox``
First, you need to locally install ``tox``
```shell
$ pip3 install tox
@@ -52,11 +66,11 @@ $ tox
### Tips
If you only want to see if you can pass the local tests, you can run ``tox -e py34``. If you just want to check for style violations, you can run ``tox -e lint``. Regardless, when you submit a pull request, your code MUST pass both the unit tests, and the linters.
If you only want to see if you can pass the local tests, you can run `tox -e py35` (or whatever python version you have installed. Only `py35`, `py36`, and `py37` will be accepted). If you just want to check for style violations, you can run `tox -e lint`. Regardless, when you submit a pull request, your code MUST pass both the unit tests, and the linters.
If you need to change anything in ``requirements.txt`` for any reason, you'll want to regenerate the virtual envrionments used by ``tox`` by running with the ``-r`` flag: ``tox -r``
If you need to change anything in `requirements.txt` for any reason, you'll want to regenerate the virtual envrionments used by `tox` by running with the `-r` flag: `tox -r`
Please do not locally disable any linter warnings within the ``blinkpy.py`` module itself (it's ok to do this in any of the ``test_*.py`` files)
If you want to run a single test (perhaps you only changed a small thing in one file) you can run `tox -e py35 -- tests/<testname>.py -x`. This will run the test `<testname>.py` and stop testing upon the first failure, making it easier to figure out why a particular test might be failing. The test structure mimics the library structure, so if you changed something in `sync_module.py`, the associated test file would be in `test_sync_module.py` (ie. the filename is prepended with `test_`.
# Catching Up With Reality
@@ -73,11 +87,3 @@ If rebase detects conflicts, repeat the following process until all changes have
2. Add the modified file: ``git add <file>`` or ``git add .``.
3. Continue rebase: ``git rebase --continue``.
4. Repeat until all conflicts resolved.
# Creating a Pull Request
Please follow these steps to create a pull request against the ``dev`` branch: [Creating a Pull Request](https://help.github.com/articles/creating-a-pull-request/)
# Monitor Build Status
Once you create your PR, you can monitor the status of your build [here](https://travis-ci.org/fronzbot/blinkpy), Your code will be tested to ensure it passes and won't cause any problems after merging.
+28 -12
View File
@@ -1,7 +1,6 @@
blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version|
================================================================
blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version| |Python Version|
================================================================================
A Python library for the Blink Camera system
Only compatible with Python 3+
Disclaimer:
~~~~~~~~~~~~~~~
@@ -32,12 +31,16 @@ To install the current development version, perform the following steps. Note t
$ pip3 install --upgrade dist/*.whl
If you'd like to contribute to this library, please read the `contributing instructions <https://github.com/fronzbot/blinkpy/blob/dev/CONTRIBUTING.md>`__.
For more information on how to use this library, please `read the docs <https://blinkpy.readthedocs.io/en/latest/>`__.
Purpose
===========
This library was built with the intention of allowing easy communication with Blink camera systems, specifically so I can add a module into homeassistant https://home-assistant.io
This library was built with the intention of allowing easy communication with Blink camera systems, specifically to support the `Blink component <https://home-assistant.io/components/blink>`__ in `homeassistant <https://home-assistant.io/>`__.
Usage
=========
Quick Start
=============
The simplest way to use this package from a terminal is to call ``Blink.start()`` which will prompt for your Blink username and password and then log you in. Alternatively, you can instantiate the Blink class with a username and password, and call ``Blink.start()`` to login and setup without prompt, as shown below. In addition, http requests are throttled internally via use of the ``Blink.refresh_rate`` variable, which can be set at initialization and defaults to 30 seconds.
.. code:: python
@@ -48,7 +51,7 @@ The simplest way to use this package from a terminal is to call ``Blink.start()`
If you would like to log in without setting up the cameras or system, you can simply call the ``Blink.login()`` function which will prompt for a username and password and then authenticate with the server. This is useful if you want to avoid use of the ``start()`` function which simply acts as a wrapper for more targeted API methods.
Cameras are instantiated as individual ``BlinkCamera`` classes within a ``BlinkSyncModule`` instance. Note: currently the API only supports one sync module, but multiple sync modules are planned to be supported in the future.
Cameras are instantiated as individual ``BlinkCamera`` classes within a ``BlinkSyncModule`` instance. All of your sync modules are stored within the ``Blink.sync`` dictionary and can be accessed using the name of the sync module as the key (this is the name of your sync module in the Blink App).
The below code will display cameras and their available attributes:
@@ -59,15 +62,15 @@ The below code will display cameras and their available attributes:
blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD')
blink.start()
for name, camera in blink.sync.cameras.items():
print(name) # Name of the camera
print(camera.attributes) # Print available attributes of camera
for name, camera in blink.cameras.items():
print(name) # Name of the camera
print(camera.attributes) # Print available attributes of camera
The most recent images and videos can be accessed as a bytes-object via internal variables. These can be updated with calls to ``Blink.refresh()`` but will only make a request if motion has been detected or other changes have been found. This can be overridden with the ``force_cache`` flag, but this should be used for debugging only since it overrides the internal request throttling.
.. code:: python
camera = blink.sync.camera['SOME CAMERA NAME']
camera = blink.cameras['SOME CAMERA NAME']
blink.refresh(force_cache=True) # force a cache update USE WITH CAUTION
camera.image_from_cache.raw # bytes-like image object (jpg)
camera.video_from_cache.raw # bytes-like video object (mp4)
@@ -76,11 +79,22 @@ The ``blinkpy`` api also allows for saving images and videos to a file and snapp
.. code:: python
camera = blink.sync.camera['SOME CAMERA NAME']
camera = blink.cameras['SOME CAMERA NAME']
camera.snap_picture() # Take a new picture with the camera
blink.refresh() # Get new information from server
camera.image_to_file('/local/path/for/image.jpg')
camera.video_to_file('/local/path/for/video.mp4')
You can also use this library to download all videos from the server. In order to do this, you must specify a ``path``. You may also specifiy a how far back in time to go to retrieve videos via the ``since=`` variable (a simple string such as ``"2017/09/21"`` is sufficient), as well as how many pages to traverse via the ``page=`` variable. Note that by default, the library will search the first ten pages which is sufficient in most use cases. Additionally, you can specidy one or more cameras via the ``camera=`` property. This can be a single string indicating the name of the camera, or a list of camera names. By default, it is set to the string ``'all'`` to grab videos from all cameras.
Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34am to the ``/home/blink`` directory:
.. code:: python
blink = blinkpy.Blink(username="YOUR USER NAME", password="YOUR PASSWORD")
blink.start()
blink.download_videos('/home/blink', since='2018/07/04 09:34')
.. |Build Status| image:: https://travis-ci.org/fronzbot/blinkpy.svg?branch=dev
:target: https://travis-ci.org/fronzbot/blinkpy
@@ -90,3 +104,5 @@ The ``blinkpy`` api also allows for saving images and videos to a file and snapp
:target: https://pypi.python.org/pypi/blinkpy
.. |Docs| image:: https://readthedocs.org/projects/blinkpy/badge/?version=latest
:target: http://blinkpy.readthedocs.io/en/latest/?badge=latest
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/blinkpy.svg
:target: https://img.shields.io/pypi/pyversions/blinkpy.svg
+146 -32
View File
@@ -3,14 +3,24 @@
import logging
from json import dumps
import blinkpy.helpers.errors as ERROR
from blinkpy.helpers.util import http_req, BlinkException
from blinkpy.helpers.util import http_req, get_time, BlinkException, Throttle
from blinkpy.helpers.constants import DEFAULT_URL
_LOGGER = logging.getLogger(__name__)
MIN_THROTTLE_TIME = 2
def request_login(blink, url, username, password):
"""Login request."""
def request_login(blink, url, username, password, is_retry=False):
"""
Login request.
:param blink: Blink instance.
:param url: Login url.
:param username: Blink username.
:param password: Blink password.
:param is_retry: Is this part of a re-authorization attempt?
"""
headers = {
'Host': DEFAULT_URL,
'Content-Type': 'application/json'
@@ -21,139 +31,243 @@ def request_login(blink, url, username, password):
'client_specifier': 'iPhone 9.2 | 2.2 | 222'
})
return http_req(blink, url=url, headers=headers, data=data,
json_resp=False, reqtype='post')
json_resp=False, reqtype='post', is_retry=is_retry)
def request_networks(blink):
"""Request network information."""
"""Request all networks information."""
url = "{}/networks".format(blink.urls.base_url)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_network_status(blink, network):
"""
Request network information.
:param blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/network/{}".format(blink.urls.base_url, network)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_syncmodule(blink, network):
"""Request sync module info."""
"""
Request sync module info.
:param blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/network/{}/syncmodules".format(blink.urls.base_url, network)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_system_arm(blink, network):
"""Arm system."""
"""
Arm system.
:param blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/network/{}/arm".format(blink.urls.base_url, network)
return http_post(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_system_disarm(blink, network):
"""Disarm system."""
"""
Disarm system.
:param blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/network/{}/disarm".format(blink.urls.base_url, network)
return http_post(blink, url)
def request_command_status(blink, network, command_id):
"""Request command status."""
url = "{}/network/{}/command_id/{}".format(blink.urls.base_url,
network,
command_id)
"""
Request command status.
:param blink: Blink instance.
:param network: Sync module network id.
:param command_id: Command id to check.
"""
url = "{}/network/{}/command/{}".format(blink.urls.base_url,
network,
command_id)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_homescreen(blink):
"""Request homescreen info."""
url = "{}/homescreen".format(blink.urls.base_url)
url = "{}/api/v3/accounts/{}/homescreen".format(blink.urls.base_url,
blink.account_id)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_sync_events(blink, network):
"""Request events from sync module."""
"""
Request events from sync module.
:param blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/events/network/{}".format(blink.urls.base_url, network)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_new_image(blink, network, camera_id):
"""Request to capture new thumbnail for camera."""
"""
Request to capture new thumbnail for camera.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request new image from.
"""
url = "{}/network/{}/camera/{}/thumbnail".format(blink.urls.base_url,
network,
camera_id)
return http_post(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_new_video(blink, network, camera_id):
"""Request to capture new video clip."""
"""
Request to capture new video clip.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request new video from.
"""
url = "{}/network/{}/camera/{}/clip".format(blink.urls.base_url,
network,
camera_id)
return http_post(blink, url)
def request_video_count(blink, headers):
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_video_count(blink):
"""Request total video count."""
url = "{}/api/v2/videos/count".format(blink.urls.base_url)
return http_get(blink, url)
def request_videos(blink, page=0):
"""Perform a request for videos."""
url = "{}/api/v2/videos/page/{}".format(blink.urls.base_url, page)
def request_videos(blink, time=None, page=0):
"""
Perform a request for videos.
:param blink: Blink instance.
:param time: Get videos since this time. In epoch seconds.
:param page: Page number to get videos from.
"""
timestamp = get_time(time)
url = "{}/api/v2/videos/changed?since={}&page={}".format(
blink.urls.base_url, timestamp, page)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_cameras(blink, network):
"""Request all camera information."""
"""
Request all camera information.
:param Blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/network/{}/cameras".format(blink.urls.base_url, network)
return http_get(blink, url)
def request_camera_info(blink, network, camera_id):
"""Request camera info for one camera."""
url = "{}/network/{}/camera/{}".format(blink.urls.base_url,
network,
camera_id)
"""
Request camera info for one camera.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request info from.
"""
url = "{}/network/{}/camera/{}/config".format(blink.urls.base_url,
network,
camera_id)
return http_get(blink, url)
def request_camera_sensors(blink, network, camera_id):
"""Request camera sensor info for one camera."""
"""
Request camera sensor info for one camera.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request sesnor info from.
"""
url = "{}/network/{}/camera/{}/signals".format(blink.urls.base_url,
network,
camera_id)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_motion_detection_enable(blink, network, camera_id):
"""Enable motion detection for a camera."""
"""
Enable motion detection for a camera.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to enable.
"""
url = "{}/network/{}/camera/{}/enable".format(blink.urls.base_url,
network,
camera_id)
return http_post(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_motion_detection_disable(blink, network, camera_id):
"""Disable motion detection for a camera."""
"""Disable motion detection for a camera.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to disable.
"""
url = "{}/network/{}/camera/{}/disable".format(blink.urls.base_url,
network,
camera_id)
return http_post(blink, url)
def http_get(blink, url, stream=False, json=True):
def http_get(blink, url, stream=False, json=True, is_retry=False):
"""
Perform an http get request.
:param url: URL to perform get request.
:param stream: Stream response? True/FALSE
:param json: Return json response? TRUE/False
:param is_retry: Is this part of a re-auth attempt?
"""
if blink.auth_header is None:
raise BlinkException(ERROR.AUTH_TOKEN)
_LOGGER.debug("Making GET request to %s", url)
return http_req(blink, url=url, headers=blink.auth_header,
reqtype='get', stream=stream, json_resp=json)
reqtype='get', stream=stream, json_resp=json,
is_retry=is_retry)
def http_post(blink, url):
def http_post(blink, url, is_retry=False):
"""
Perform an http post request.
:param url: URL to perfom post request.
:param is_retry: Is this part of a re-auth attempt?
"""
if blink.auth_header is None:
raise BlinkException(ERROR.AUTH_TOKEN)
return http_req(blink, url=url, headers=blink.auth_header, reqtype='post')
_LOGGER.debug("Making POST request to %s", url)
return http_req(blink, url=url, headers=blink.auth_header,
reqtype='post', is_retry=is_retry)
+209 -58
View File
@@ -1,31 +1,44 @@
# -*- coding: utf-8 -*-
"""
blinkpy by Kevin Fronczak - A Blink camera Python library.
blinkpy is an unofficial api for the Blink security camera system.
repo url: https://github.com/fronzbot/blinkpy
https://github.com/fronzbot/blinkpy
Original protocol hacking by MattTW :
https://github.com/MattTW/BlinkMonitorProtocol
Published under the MIT license - See LICENSE file for more details.
"Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark
owned by Immedia Inc., see www.blinkforhome.com for more information.
I am in no way affiliated with Blink, nor Immedia Inc.
blinkpy is in no way affiliated with Blink, nor Immedia Inc.
"""
import os.path
import time
import getpass
import logging
import blinkpy.helpers.errors as ERROR
from shutil import copyfileobj
from requests.structures import CaseInsensitiveDict
from dateutil.parser import parse
from blinkpy import api
from blinkpy.sync_module import BlinkSyncModule
from blinkpy.helpers import errors as ERROR
from blinkpy.helpers.util import (
create_session, BlinkURLHandler,
BlinkAuthenticationException)
create_session, merge_dicts, get_time, BlinkURLHandler,
BlinkAuthenticationException, Throttle)
from blinkpy.helpers.constants import (
BLINK_URL, LOGIN_URL, LOGIN_BACKUP_URL)
BLINK_URL, LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL)
from blinkpy.helpers.constants import __version__
REFRESH_RATE = 30
_LOGGER = logging.getLogger('blinkpy')
# Prevents rapid calls to blink.refresh()
# with the force_cache flag set to True
MIN_THROTTLE_TIME = 2
_LOGGER = logging.getLogger(__name__)
class Blink():
@@ -46,17 +59,20 @@ class Blink():
self._token = None
self._auth_header = None
self._host = None
self.network_id = None
self.account_id = None
self.network_ids = []
self.urls = None
self.sync = None
self.sync = CaseInsensitiveDict({})
self.region = None
self.region_id = None
self.last_refresh = None
self.refresh_rate = refresh_rate
self.session = None
self.session = create_session()
self.networks = []
self.cameras = CaseInsensitiveDict({})
self.video_list = CaseInsensitiveDict({})
self._login_url = LOGIN_URL
self.version = __version__
@property
def auth_header(self):
@@ -71,84 +87,133 @@ class Blink():
Essentially this is just a wrapper function for ease of use.
"""
if self._username is None or self._password is None:
self.login()
else:
self.get_auth_token()
if not self.login():
return
elif not self.get_auth_token():
return
self.get_ids()
self.sync = BlinkSyncModule(self)
self.sync.start()
camera_list = self.get_cameras()
networks = self.get_ids()
for network_name, network_id in networks.items():
if network_id not in camera_list.keys():
camera_list[network_id] = {}
_LOGGER.warning("No cameras found for %s", network_name)
sync_module = BlinkSyncModule(self,
network_name,
network_id,
camera_list[network_id])
sync_module.start()
self.sync[network_name] = sync_module
self.cameras = self.merge_cameras()
def login(self):
"""Prompt user for username and password."""
self._username = input("Username:")
self._password = getpass.getpass("Password:")
if self.get_auth_token():
_LOGGER.info("Login successful!")
_LOGGER.debug("Login successful!")
return True
_LOGGER.warning("Unable to login with %s.", self._username)
return False
def get_auth_token(self):
def get_auth_token(self, is_retry=False):
"""Retrieve the authentication token from Blink."""
if not isinstance(self._username, str):
raise BlinkAuthenticationException(ERROR.USERNAME)
if not isinstance(self._password, str):
raise BlinkAuthenticationException(ERROR.PASSWORD)
login_url = LOGIN_URL
self.session = create_session()
response = api.request_login(self,
login_url,
self._username,
self._password)
login_urls = [LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL]
if response.status_code == 200:
response = response.json()
(self.region_id, self.region), = response['region'].items()
else:
_LOGGER.debug(
("Received response code %s "
"when authenticating, "
"trying new url"), response.status_code
)
login_url = LOGIN_BACKUP_URL
response = api.request_login(self,
login_url,
self._username,
self._password)
self.region_id = 'piri'
self.region = "UNKNOWN"
response = self.login_request(login_urls, is_retry=is_retry)
if not response:
return False
self._host = "{}.{}".format(self.region_id, BLINK_URL)
self._token = response['authtoken']['authtoken']
self.networks = response['networks']
self._auth_header = {'Host': self._host,
'TOKEN_AUTH': self._token}
self.networks = response['networks']
self.urls = BlinkURLHandler(self.region_id)
self._login_url = login_url
return self._auth_header
def login_request(self, login_urls, is_retry=False):
"""Make a login request."""
try:
login_url = login_urls.pop(0)
except IndexError:
_LOGGER.error("Could not login to blink servers.")
return False
_LOGGER.info("Attempting login with %s", login_url)
response = api.request_login(self,
login_url,
self._username,
self._password,
is_retry=is_retry)
try:
if response.status_code != 200:
response = self.login_request(login_urls)
response = response.json()
(self.region_id, self.region), = response['region'].items()
except AttributeError:
_LOGGER.error("Login API endpoint failed with response %s",
response,
exc_info=True)
return False
except KeyError:
_LOGGER.warning("Could not extract region info.")
self.region_id = 'piri'
self.region = 'UNKNOWN'
self._login_url = login_url
return response
def get_ids(self):
"""Set the network ID and Account ID."""
response = api.request_networks(self)
# Look for only onboarded network, flag warning if multiple
# since it's unexpected
all_networks = []
network_dict = {}
for network, status in self.networks.items():
if status['onboarded']:
all_networks.append(network)
self.network_id = all_networks.pop(0)
for resp in response['networks']:
if str(resp['id']) == self.network_id:
self.account_id = resp['account_id']
if all_networks:
_LOGGER.error(("More than one unboarded network. "
"Platform may not work as intended. "
"Please open an issue on "
"https://github.com/fronzbot/blinkpy."))
all_networks.append('{}'.format(network))
network_dict[status['name']] = network
# For the first onboarded network we find, grab the account id
for resp in response['networks']:
if str(resp['id']) in all_networks:
self.account_id = resp['account_id']
break
self.network_ids = all_networks
return network_dict
def get_cameras(self):
"""Retrieve a camera list for each onboarded network."""
response = api.request_homescreen(self)
try:
all_cameras = {}
for camera in response['cameras']:
camera_network = str(camera['network_id'])
camera_name = camera['name']
camera_id = camera['id']
camera_info = {'name': camera_name, 'id': camera_id}
if camera_network not in all_cameras:
all_cameras[camera_network] = []
all_cameras[camera_network].append(camera_info)
return all_cameras
except KeyError:
_LOGGER.error("Initialization failue. Could not retrieve cameras.")
return {}
@Throttle(seconds=MIN_THROTTLE_TIME)
def refresh(self, force_cache=False):
"""
Perform a system refresh.
@@ -156,8 +221,14 @@ class Blink():
:param force_cache: Force an update of the camera cache
"""
if self.check_if_ok_to_update() or force_cache:
_LOGGER.debug("Attempting refresh of cameras.")
self.sync.refresh(force_cache=force_cache)
for sync_name, sync_module in self.sync.items():
_LOGGER.debug("Attempting refresh of sync %s", sync_name)
sync_module.refresh(force_cache=force_cache)
if not force_cache:
# Prevents rapid clearing of motion detect property
self.last_refresh = int(time.time())
return True
return False
def check_if_ok_to_update(self):
"""Check if it is ok to perform an http request."""
@@ -166,6 +237,86 @@ class Blink():
if last_refresh is None:
last_refresh = 0
if current_time >= (last_refresh + self.refresh_rate):
self.last_refresh = current_time
return True
return False
def merge_cameras(self):
"""Merge all sync camera dicts into one."""
combined = CaseInsensitiveDict({})
for sync in self.sync:
combined = merge_dicts(combined, self.sync[sync].cameras)
return combined
def download_videos(self, path, since=None, camera='all', stop=10):
"""
Download all videos from server since specified time.
:param path: Path to write files. /path/<cameraname>_<recorddate>.mp4
:param since: Date and time to get videos from.
Ex: "2018/07/28 12:33:00" to retrieve videos since
July 28th 2018 at 12:33:00
:param camera: Camera name to retrieve. Defaults to "all".
Use a list for multiple cameras.
:param stop: Page to stop on (~25 items per page. Default page 10).
"""
if since is None:
since_epochs = self.last_refresh
else:
parsed_datetime = parse(since, fuzzy=True)
since_epochs = parsed_datetime.timestamp()
formatted_date = get_time(time_to_convert=since_epochs)
_LOGGER.info("Retrieving videos since %s", formatted_date)
if not isinstance(camera, list):
camera = [camera]
for page in range(1, stop):
response = api.request_videos(self, time=since_epochs, page=page)
_LOGGER.debug("Processing page %s", page)
try:
result = response['videos']
if not result:
raise IndexError
except (KeyError, IndexError):
_LOGGER.info("No videos found on page %s. Exiting.", page)
break
self._parse_downloaded_items(result, camera, path)
def _parse_downloaded_items(self, result, camera, path):
"""Parse downloaded videos."""
for item in result:
try:
created_at = item['created_at']
camera_name = item['camera_name']
is_deleted = item['deleted']
address = item['address']
except KeyError:
_LOGGER.info("Missing clip information, skipping...")
continue
if camera_name not in camera and 'all' not in camera:
_LOGGER.debug("Skipping videos for %s.", camera_name)
continue
if is_deleted:
_LOGGER.debug("%s: %s is marked as deleted.",
camera_name,
address)
continue
clip_address = "{}{}".format(self.urls.base_url, address)
filename = "{}_{}.mp4".format(camera_name, created_at)
filename = os.path.join(path, filename)
if os.path.isfile(filename):
_LOGGER.info("%s already exists, skipping...", filename)
continue
response = api.http_get(self, url=clip_address,
stream=True, json=False)
with open(filename, 'wb') as vidfile:
copyfileobj(response.raw, vidfile)
_LOGGER.info("Downloaded video to %s", filename)
+35 -31
View File
@@ -6,8 +6,6 @@ from blinkpy import api
_LOGGER = logging.getLogger(__name__)
MAX_CLIPS = 5
class BlinkCamera():
"""Class to initialize individual camera."""
@@ -24,10 +22,11 @@ class BlinkCamera():
self.battery_voltage = None
self.clip = None
self.temperature = None
self.temperature_calibrated = None
self.battery_state = None
self.motion_detected = None
self.wifi_strength = None
self.last_record = []
self.last_record = None
self._cached_image = None
self._cached_video = None
@@ -40,6 +39,7 @@ class BlinkCamera():
'serial': self.serial,
'temperature': self.temperature,
'temperature_c': self.temperature_c,
'temperature_calibrated': self.temperature_calibrated,
'battery': self.battery,
'thumbnail': self.thumbnail,
'video': self.clip,
@@ -47,6 +47,7 @@ class BlinkCamera():
'motion_detected': self.motion_detected,
'wifi_strength': self.wifi_strength,
'network_id': self.sync.network_id,
'sync_module': self.sync.name,
'last_record': self.last_record
}
return attributes
@@ -91,10 +92,11 @@ class BlinkCamera():
self.network_id,
self.camera_id)
def update(self, config, force_cache=False):
def update(self, config, force_cache=False, **kwargs):
"""Update camera info."""
# force = kwargs.pop('force', False)
self.name = config['name']
self.camera_id = str(config['camera_id'])
self.camera_id = str(config['id'])
self.network_id = str(config['network_id'])
self.serial = config['serial']
self.motion_enabled = config['enabled']
@@ -103,6 +105,16 @@ class BlinkCamera():
self.temperature = config['temperature']
self.wifi_strength = config['wifi_strength']
# Retrieve calibrated temperature from special endpoint
resp = api.request_camera_sensors(self.sync.blink,
self.network_id,
self.camera_id)
try:
self.temperature_calibrated = resp['temp']
except KeyError:
self.temperature_calibrated = self.temperature
_LOGGER.warning("Could not retrieve calibrated temperature.")
# Check if thumbnail exists in config, if not try to
# get it from the homescreen info in teh sync module
# otherwise set it to None and log an error
@@ -116,12 +128,15 @@ class BlinkCamera():
new_thumbnail = "{}{}.jpg".format(self.sync.urls.base_url,
thumb_addr)
# Check if a new motion clip has been recorded
# check_for_motion_method sets motion_detected variable
self.check_for_motion()
try:
self.motion_detected = self.sync.motion[self.name]
except KeyError:
self.motion_detected = False
clip_addr = None
if self.last_record:
clip_addr = self.sync.all_clips[self.name][self.last_record[0]]
if self.name in self.sync.last_record:
clip_addr = self.sync.last_record[self.name]['clip']
self.last_record = self.sync.last_record[self.name]['time']
self.clip = "{}{}".format(self.sync.urls.base_url,
clip_addr)
@@ -146,25 +161,6 @@ class BlinkCamera():
stream=True,
json=False)
def check_for_motion(self):
"""Check if motion detected.."""
try:
records = sorted(self.sync.record_dates[self.name])
new_clip = records.pop()
if new_clip not in self.last_record and self.last_record:
self.motion_detected = True
self.last_record.insert(0, new_clip)
if len(self.last_record) > MAX_CLIPS:
self.last_record.pop()
elif not self.last_record:
self.last_record.insert(0, new_clip)
self.motion_detected = False
else:
self.motion_detected = False
except KeyError:
_LOGGER.warning("Could not extract clip info from camera %s",
self.name)
def image_to_file(self, path):
"""
Write image to file.
@@ -178,7 +174,8 @@ class BlinkCamera():
copyfileobj(response.raw, imgfile)
else:
_LOGGER.error("Cannot write image to file, response %s",
response.status_code)
response.status_code,
exc_info=True)
def video_to_file(self, path):
"""Write video to file.
@@ -187,6 +184,11 @@ class BlinkCamera():
"""
_LOGGER.debug("Writing video from %s to %s", self.name, path)
response = self._cached_video
if response is None:
_LOGGER.error("No saved video exist for %s.",
self.name,
exc_info=True)
return
with open(path, 'wb') as vidfile:
copyfileobj(response.raw, vidfile)
@@ -201,5 +203,7 @@ class BlinkCamera():
return device_thumb
except KeyError:
pass
_LOGGER.error("Could not find thumbnail for camera %s", self.name)
_LOGGER.error("Could not find thumbnail for camera %s",
self.name,
exc_info=True)
return None
+12 -6
View File
@@ -3,8 +3,8 @@
import os
MAJOR_VERSION = 0
MINOR_VERSION = 10
PATCH_VERSION = 0
MINOR_VERSION = 13
PATCH_VERSION = 1
__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
@@ -45,12 +45,18 @@ 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
DEFAULT_URL = "{}.{}".format('prod', BLINK_URL)
BASE_URL = "https://{}".format(DEFAULT_URL)
LOGIN_URL = "{}/api/v2/login".format(BASE_URL)
OLD_LOGIN_URL = "{}/login".format(BASE_URL)
LOGIN_BACKUP_URL = "https://{}.{}/login".format('rest.piri', BLINK_URL)
'''
Dictionaries
'''
ONLINE = {'online': True, 'offline': False}
'''
OTHER
'''
TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S%Z'
+68 -15
View File
@@ -1,14 +1,32 @@
"""Useful functions for blinkpy."""
import logging
import time
from functools import wraps
from requests import Request, Session, exceptions
from blinkpy.helpers.constants import BLINK_URL
from blinkpy.helpers.constants import BLINK_URL, TIMESTAMP_FORMAT
import blinkpy.helpers.errors as ERROR
_LOGGER = logging.getLogger(__name__)
def get_time(time_to_convert=None):
"""Create blink-compatible timestamp."""
if time_to_convert is None:
time_to_convert = time.time()
return time.strftime(TIMESTAMP_FORMAT, time.localtime(time_to_convert))
def merge_dicts(dict_a, dict_b):
"""Merge two dictionaries into one."""
duplicates = [val for val in dict_a if val in dict_b]
if duplicates:
_LOGGER.warning(("Duplicates found during merge: %s. "
"Renaming is recommended."), duplicates)
return {**dict_a, **dict_b}
def create_session():
"""Create a session for blink communication."""
sess = Session()
@@ -17,8 +35,8 @@ def create_session():
def attempt_reauthorization(blink):
"""Attempt to refresh auth token and links."""
_LOGGER.debug("Auth token expired, attempting reauthorization.")
headers = blink.get_auth_token()
_LOGGER.info("Auth token expired, attempting reauthorization.")
headers = blink.get_auth_token(is_retry=True)
return headers
@@ -47,21 +65,28 @@ def http_req(blink, url='http://example.com', data=None, headers=None,
prepped = req.prepare()
try:
response = blink.session.send(prepped, stream=stream)
response = blink.session.send(prepped, stream=stream, timeout=10)
if json_resp and 'code' in response.json():
if is_retry:
_LOGGER.error("Cannot obtain new token for server auth.")
return None
else:
headers = attempt_reauthorization(blink)
if not headers:
raise exceptions.ConnectionError
return http_req(blink, url=url, data=data, headers=headers,
reqtype=reqtype, stream=stream,
json_resp=json_resp, is_retry=True)
except (exceptions.ConnectionError, exceptions.Timeout):
_LOGGER.error("Cannot connect to server. Possible outage.")
return None
if json_resp and 'code' in response.json():
if is_retry:
_LOGGER.error("Cannot authenticate with server.")
raise BlinkAuthenticationException(
(response.json()['code'], response.json()['message']))
else:
_LOGGER.info("Cannot connect to server with url %s.", url)
if not is_retry:
headers = attempt_reauthorization(blink)
return http_req(blink, url=url, data=data, headers=headers,
reqtype=reqtype, stream=stream,
json_resp=json_resp, is_retry=True)
_LOGGER.error("Endpoint %s failed. Possible issue with Blink servers.",
url)
return None
if json_resp:
return response.json()
@@ -82,8 +107,6 @@ class BlinkException(Exception):
class BlinkAuthenticationException(BlinkException):
"""Class to throw authentication exception."""
pass
class BlinkURLHandler():
"""Class that handles Blink URLS."""
@@ -97,3 +120,33 @@ class BlinkURLHandler():
self.networks_url = "{}/networks".format(self.base_url)
self.video_url = "{}/api/v2/videos".format(self.base_url)
_LOGGER.debug("Setting base url to %s.", self.base_url)
class Throttle():
"""Class for throttling api calls."""
def __init__(self, seconds=10):
"""Initialize throttle class."""
self.throttle_time = seconds
self.last_call = 0
def __call__(self, method):
"""Throttle caller method."""
def throttle_method():
"""Call when method is throttled."""
return None
@wraps(method)
def wrapper(*args, **kwargs):
"""Wrap that checks for throttling."""
force = kwargs.pop('force', False)
now = int(time.time())
last_call_delta = now - self.last_call
if force or last_call_delta > self.throttle_time:
result = method(*args, *kwargs)
self.last_call = now
return result
return throttle_method()
return wrapper
+102 -86
View File
@@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__)
class BlinkSyncModule():
"""Class to initialize sync module."""
def __init__(self, blink):
def __init__(self, blink, network_name, network_id, camera_list):
"""
Initialize Blink sync module.
@@ -21,21 +21,21 @@ class BlinkSyncModule():
"""
self.blink = blink
self._auth_header = blink.auth_header
self.network_id = blink.network_id
self.network_id = network_id
self.region = blink.region
self.region_id = blink.region_id
self.name = 'sync'
self.name = network_name
self.serial = None
self.status = None
self.sync_id = None
self.host = None
self.summary = None
self.homescreen = None
self.record_dates = {}
self.videos = {}
self.network_info = None
self.events = []
self.cameras = CaseInsensitiveDict({})
self.all_clips = {}
self.motion = {}
self.last_record = {}
self.camera_list = camera_list
@property
def attributes(self):
@@ -64,7 +64,10 @@ class BlinkSyncModule():
@property
def arm(self):
"""Return status of sync module: armed/disarmed."""
return self.homescreen['network']['armed']
try:
return self.network_info['network']['armed']
except (KeyError, TypeError):
return None
@arm.setter
def arm(self, value):
@@ -76,97 +79,110 @@ class BlinkSyncModule():
def start(self):
"""Initialize the system."""
response = api.request_syncmodule(self.blink, self.network_id)
self.summary = response['syncmodule']
self.name = self.summary['name']
self.sync_id = self.summary['id']
self.network_id = self.summary['network_id']
self.serial = self.summary['serial']
self.status = self.summary['status']
response = api.request_syncmodule(self.blink,
self.network_id,
force=True)
try:
self.summary = response['syncmodule']
self.network_id = self.summary['network_id']
except (TypeError, KeyError):
_LOGGER.error(("Could not retrieve sync module information "
"with response: %s"), response, exc_info=True)
return False
self.events = self.get_events()
try:
self.sync_id = self.summary['id']
self.serial = self.summary['serial']
self.status = self.summary['status']
except KeyError:
_LOGGER.error("Could not extract some sync module info: %s",
response,
exc_info=True)
self.homescreen = api.request_homescreen(self.blink)
self.network_info = api.request_network_status(self.blink,
self.network_id)
camera_info = self.get_camera_info()
for camera_config in camera_info:
name = camera_config['name']
self.cameras[name] = BlinkCamera(self)
self.check_new_videos()
try:
for camera_config in self.camera_list:
if 'name' not in camera_config:
break
name = camera_config['name']
self.cameras[name] = BlinkCamera(self)
self.motion[name] = False
camera_info = self.get_camera_info(camera_config['id'])
self.cameras[name].update(camera_info,
force_cache=True,
force=True)
except KeyError:
_LOGGER.error("Could not create cameras instances for %s",
self.name,
exc_info=True)
return False
self.videos = self.get_videos()
for camera_config in camera_info:
name = camera_config['name']
if name in self.cameras:
self.cameras[name].update(camera_config, force_cache=True)
return True
def get_events(self):
def get_events(self, **kwargs):
"""Retrieve events from server."""
response = api.request_sync_events(self.blink, self.network_id)
return response['event']
force = kwargs.pop('force', False)
response = api.request_sync_events(self.blink,
self.network_id,
force=force)
try:
return response['event']
except (TypeError, KeyError):
_LOGGER.error("Could not extract events: %s",
response,
exc_info=True)
return False
def get_camera_info(self):
def get_camera_info(self, camera_id):
"""Retrieve camera information."""
response = api.request_cameras(self.blink, self.network_id)
return response['devicestatus']
response = api.request_camera_info(self.blink,
self.network_id,
camera_id)
try:
return response['camera'][0]
except (TypeError, KeyError):
_LOGGER.error("Could not extract camera info: %s",
response,
exc_info=True)
return []
def refresh(self, force_cache=False):
"""Get all blink cameras and pulls their most recent status."""
self.events = self.get_events()
self.videos = self.get_videos()
self.homescreen = api.request_homescreen(self.blink)
camera_info = self.get_camera_info()
for camera_config in camera_info:
name = camera_config['name']
self.cameras[name].update(camera_config, force_cache=force_cache)
self.network_info = api.request_network_status(self.blink,
self.network_id)
self.check_new_videos()
for camera_name in self.cameras.keys():
camera_id = self.cameras[camera_name].camera_id
camera_info = self.get_camera_info(camera_id)
self.cameras[camera_name].update(camera_info,
force_cache=force_cache)
def get_videos(self, start_page=0, end_page=1):
"""
Retrieve last recorded videos per camera.
def check_new_videos(self):
"""Check if new videos since last refresh."""
resp = api.request_videos(self.blink,
time=self.blink.last_refresh,
page=0)
:param start_page: Page to start reading from on blink servers
(defaults to 0)
:param end_page: Page to stop reading from (defaults to 1)
"""
videos = list()
all_dates = dict()
for camera in self.cameras.keys():
self.motion[camera] = False
for page_num in range(start_page, end_page + 1):
this_page = api.request_videos(self.blink, page=page_num)
if not this_page:
break
videos.append(this_page)
try:
info = resp['videos']
except (KeyError, TypeError):
_LOGGER.warning("Could not check for motion. Response: %s", resp)
return False
for page in videos:
_LOGGER.debug("Retrieved video page %s", page)
for entry in page:
camera_name = entry['camera_name']
clip_addr = entry['address']
thumb_addr = entry['thumbnail']
clip_date = clip_addr.split('_')[-6:]
clip_date = '_'.join(clip_date)
clip_date = clip_date.split('.')[0]
try:
self.all_clips[camera_name][clip_date] = clip_addr
except KeyError:
self.all_clips[camera_name] = {clip_date: clip_addr}
for entry in info:
try:
name = entry['camera_name']
clip = entry['address']
timestamp = entry['created_at']
self.motion[name] = True
self.last_record[name] = {'clip': clip, 'time': timestamp}
except KeyError:
_LOGGER.debug("No new videos since last refresh.")
if camera_name not in all_dates:
all_dates[camera_name] = list()
all_dates[camera_name].append(clip_date)
try:
self.videos[camera_name].append(
{
'clip': clip_addr,
'thumb': thumb_addr,
}
)
except KeyError:
self.videos[camera_name] = [
{
'clip': clip_addr,
'thumb': thumb_addr,
}
]
self.record_dates = all_dates
return self.videos
return True
+4 -17
View File
@@ -1,30 +1,17 @@
.. _core_module:
:mod:`blinkpy`
Blinkpy
----------------------
.. automodule:: blinkpy.blinkpy
:members:
.. automodule:: blinkpy.sync_module
:members:
.. automodule:: blinkpy.camera
:members:
.. automodule:: blinkpy.helpers.util
.. autoclass:: blinkpy.blinkpy.Blink
:members:
.. autoclass:: blinkpy.sync_module.BlinkSyncModule
:members:
.. autoclass:: blinkpy.camera.BlinkCamera
:members:
.. autoclass:: blinkpy.helpers.util.BlinkURLHandler
:members:
.. autofunction:: blinkpy.helpers.util.create_session
.. autofunction:: blinkpy.helpers.util.http_req
.. autofunction:: blinkpy.helpers.util.attempt_reauthorization
+6
View File
@@ -0,0 +1,6 @@
API Reference
----------------------
.. automodule:: blinkpy.api
:members:
+4
View File
@@ -8,6 +8,8 @@ reports=no
# too-many-* - are not enforced for the sake of readability
# too-few-* - same as too-many-*
# no-else-return - I don't see any reason to enforce this. both forms are readable
# no-self-use - stupid and only annoying
# unexpected-keyword-arg - doesn't allow for use of **kwargs, which is dumb
disable=
locally-disabled,
@@ -23,3 +25,5 @@ disable=
too-many-lines,
too-few-public-methods,
no-else-return,
no-self-use,
unexpected-keyword-arg,
+3 -2
View File
@@ -1,2 +1,3 @@
requests>=2.12.4
testtools
python-dateutil==2.7.5
requests>=2.20.0
testtools==2.3.0
+1 -1
View File
@@ -1,6 +1,6 @@
flake8==3.5.0
flake8-docstrings==1.3.0
pylint==2.1.1
pylint==2.3.0
pydocstyle==2.1.1
pytest==3.7.1
pytest-cov>=2.3.1
-2
View File
@@ -67,5 +67,3 @@ def mocked_session_send(*args, **kwargs):
class MockURLHandler(BlinkURLHandler):
"""Mocks URL Handler in blinkpy module."""
pass
+15 -5
View File
@@ -1,6 +1,7 @@
"""Test various api functions."""
import unittest
from unittest import mock
from blinkpy import api
from blinkpy.blinkpy import Blink
from blinkpy.helpers.util import create_session
@@ -20,13 +21,22 @@ class TestBlinkAPI(unittest.TestCase):
"""Tear down blink module."""
self.blink = None
def test_http_req_connect_error(self):
@mock.patch('blinkpy.blinkpy.Blink.get_auth_token')
def test_http_req_connect_error(self, mock_auth):
"""Test http_get error condition."""
expected = ("ERROR:blinkpy.helpers.util:"
"Cannot connect to server. Possible outage.")
mock_auth.return_value = {'foo': 'bar'}
firstlog = ("INFO:blinkpy.helpers.util:"
"Cannot connect to server with url {}.").format(
'http://notreal.fake')
nextlog = ("INFO:blinkpy.helpers.util:"
"Auth token expired, attempting reauthorization.")
lastlog = ("ERROR:blinkpy.helpers.util:"
"Endpoint {} failed. Possible issue with "
"Blink servers.").format('http://notreal.fake')
expected = [firstlog, nextlog, firstlog, lastlog]
with self.assertLogs() as getlog:
api.http_get(self.blink, 'http://notreal.fake')
with self.assertLogs() as postlog:
api.http_post(self.blink, 'http://notreal.fake')
self.assertEqual(getlog.output, [expected])
self.assertEqual(postlog.output, [expected])
self.assertEqual(getlog.output, expected)
self.assertEqual(postlog.output, expected)
+106 -12
View File
@@ -1,12 +1,11 @@
"""Tests camera and system functions."""
import unittest
from unittest import mock
from requests import Request
import logging
from blinkpy import blinkpy
from blinkpy.sync_module import BlinkSyncModule
from blinkpy.helpers.util import create_session
from blinkpy.helpers.util import create_session, get_time
import tests.mock_responses as mresp
USERNAME = 'foobar'
@@ -56,17 +55,112 @@ class TestBlinkFunctions(unittest.TestCase):
"""Clean up after test."""
self.blink = None
@mock.patch('blinkpy.blinkpy.api.http_req')
@mock.patch('blinkpy.blinkpy.api.request_login')
def test_backup_url(self, req, mock_sess):
"""Test backup login method."""
fake_req = Request('POST', 'http://wrong.url').prepare()
json_resp = {
'authtoken': {'authtoken': 'foobar123'},
'networks': {'1234': {'name': 'foobar', 'onboarded': True}}
}
bad_req = mresp.MockResponse({}, 404)
new_req = mresp.MockResponse(json_resp, 200)
req.side_effect = [
mresp.mocked_session_send(fake_req),
{'authtoken': {'authtoken': 'foobar123'},
'networks': {'1234': {'name': 'foobar', 'onboarded': True}}}
bad_req,
bad_req,
new_req
]
self.blink.get_auth_token()
self.assertEqual(self.blink.region_id, 'piri')
self.assertEqual(self.blink.region, 'UNKNOWN')
self.blink.login_request(['test1', 'test2', 'test3'])
# pylint: disable=protected-access
self.assertEqual(self.blink._token, 'foobar123')
self.assertEqual(self.blink._login_url, 'test3')
req.side_effect = [
bad_req,
new_req,
bad_req
]
self.blink.login_request(['test1', 'test2', 'test3'])
# pylint: disable=protected-access
self.assertEqual(self.blink._login_url, 'test2')
def test_merge_cameras(self, mock_sess):
"""Test merge camera functionality."""
first_dict = {'foo': 'bar', 'test': 123}
next_dict = {'foobar': 456, 'bar': 'foo'}
self.blink.sync['foo'] = BlinkSyncModule(self.blink, 'foo', 1, [])
self.blink.sync['bar'] = BlinkSyncModule(self.blink, 'bar', 2, [])
self.blink.sync['foo'].cameras = first_dict
self.blink.sync['bar'].cameras = next_dict
result = self.blink.merge_cameras()
expected = {'foo': 'bar', 'test': 123, 'foobar': 456, 'bar': 'foo'}
self.assertEqual(expected, result)
@mock.patch('blinkpy.blinkpy.api.request_videos')
def test_download_video_exit(self, mock_req, mock_sess):
"""Test we exit method when provided bad response."""
blink = blinkpy.Blink()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
blink.last_refresh = 0
mock_req.return_value = {}
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"INFO:blinkpy.blinkpy:No videos found on page 1. Exiting."
]
with self.assertLogs() as dl_log:
blink.download_videos('/tmp')
self.assertEqual(dl_log.output, expected_log)
@mock.patch('blinkpy.blinkpy.api.request_videos')
def test_parse_downloaded_items(self, mock_req, mock_sess):
"""Test ability to parse downloaded items list."""
blink = blinkpy.Blink()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
generic_entry = {
'created_at': '1970',
'camera_name': 'foo',
'deleted': True,
'address': '/bar.mp4'
}
result = [generic_entry]
mock_req.return_value = {'videos': result}
blink.last_refresh = 0
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"DEBUG:blinkpy.blinkpy:foo: /bar.mp4 is marked as deleted."
]
with self.assertLogs() as dl_log:
blink.download_videos('/tmp', stop=2)
self.assertEqual(dl_log.output, expected_log)
@mock.patch('blinkpy.blinkpy.api.request_videos')
def test_parse_camera_not_in_list(self, mock_req, mock_sess):
"""Test ability to parse downloaded items list."""
blink = blinkpy.Blink()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
generic_entry = {
'created_at': '1970',
'camera_name': 'foo',
'deleted': True,
'address': '/bar.mp4'
}
result = [generic_entry]
mock_req.return_value = {'videos': result}
blink.last_refresh = 0
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"DEBUG:blinkpy.blinkpy:Skipping videos for foo."
]
with self.assertLogs() as dl_log:
blink.download_videos('/tmp', camera='bar', stop=2)
self.assertEqual(dl_log.output, expected_log)
+66 -8
View File
@@ -14,6 +14,7 @@ from blinkpy.sync_module import BlinkSyncModule
from blinkpy.helpers.util import (
http_req, create_session, BlinkAuthenticationException,
BlinkException, BlinkURLHandler)
from blinkpy.helpers.constants import __version__
import tests.mock_responses as mresp
USERNAME = 'foobar'
@@ -30,7 +31,10 @@ class TestBlinkSetup(unittest.TestCase):
self.blink_no_cred = Blink()
self.blink = Blink(username=USERNAME,
password=PASSWORD)
self.blink.sync = BlinkSyncModule(self.blink)
self.blink.sync['test'] = BlinkSyncModule(self.blink,
'test',
'1234',
[])
self.blink.urls = BlinkURLHandler('test')
self.blink.session = create_session()
@@ -41,6 +45,7 @@ class TestBlinkSetup(unittest.TestCase):
def test_initialization(self, mock_sess):
"""Verify we can initialize blink."""
self.assertEqual(self.blink.version, __version__)
# pylint: disable=protected-access
self.assertEqual(self.blink._username, USERNAME)
# pylint: disable=protected-access
@@ -77,11 +82,14 @@ class TestBlinkSetup(unittest.TestCase):
def test_bad_request(self, mock_sess):
"""Check that we raise an Exception with a bad request."""
self.blink.session = create_session()
explog = ("ERROR:blinkpy.helpers.util:"
"Cannot obtain new token for server auth.")
with self.assertRaises(BlinkException):
http_req(self.blink, reqtype='bad')
with self.assertRaises(BlinkAuthenticationException):
with self.assertLogs() as logrecord:
http_req(self.blink, reqtype='post', is_retry=True)
self.assertEqual(logrecord.output, [explog])
def test_authentication(self, mock_sess):
"""Check that we can authenticate Blink up properly."""
@@ -107,21 +115,71 @@ class TestBlinkSetup(unittest.TestCase):
'networks': [{'id': 1234, 'account_id': 1111},
{'id': 5678, 'account_id': 2222}]
}
self.blink.networks = {'0000': {'onboarded': False},
'5678': {'onboarded': True},
'1234': {'onboarded': False}}
self.blink.networks = {'0000': {'onboarded': False, 'name': 'foo'},
'5678': {'onboarded': True, 'name': 'bar'},
'1234': {'onboarded': False, 'name': 'test'}}
self.blink.get_ids()
self.assertEqual(self.blink.network_id, '5678')
self.assertTrue('5678' in self.blink.network_ids)
self.assertEqual(self.blink.account_id, 2222)
@mock.patch('blinkpy.api.request_networks')
def test_multiple_onboarded_networks(self, mock_net, mock_sess):
"""Check that we handle multiple networks appropriately."""
mock_net.return_value = {
'networks': [{'id': 0000, 'account_id': 2222},
{'id': 5678, 'account_id': 1111}]
}
self.blink.networks = {'0000': {'onboarded': False, 'name': 'foo'},
'5678': {'onboarded': True, 'name': 'bar'},
'1234': {'onboarded': True, 'name': 'test'}}
self.blink.get_ids()
self.assertTrue('5678' in self.blink.network_ids)
self.assertTrue('1234' in self.blink.network_ids)
self.assertEqual(self.blink.account_id, 1111)
@mock.patch('blinkpy.blinkpy.time.time')
def test_throttle(self, mock_time, mock_sess):
"""Check throttling functionality."""
now = self.blink.refresh_rate + 1
mock_time.return_value = now
self.assertEqual(self.blink.last_refresh, None)
result = self.blink.check_if_ok_to_update()
self.assertEqual(self.blink.check_if_ok_to_update(), True)
self.assertEqual(self.blink.last_refresh, None)
with mock.patch('blinkpy.sync_module.BlinkSyncModule.refresh',
return_value=True):
self.blink.refresh()
self.assertEqual(self.blink.last_refresh, now)
self.assertEqual(result, True)
self.assertEqual(self.blink.check_if_ok_to_update(), False)
self.assertEqual(self.blink.last_refresh, now)
def test_sync_case_insensitive_dict(self, mock_sess):
"""Check that we can access sync modules ignoring case."""
self.assertEqual(self.blink.sync['test'].name, 'test')
self.assertEqual(self.blink.sync['TEST'].name, 'test')
@mock.patch('blinkpy.api.request_login')
def test_unexpected_login(self, mock_login, mock_sess):
"""Check that we appropriately handle unexpected login info."""
mock_login.return_value = None
self.assertFalse(self.blink.get_auth_token())
@mock.patch('blinkpy.api.request_homescreen')
def test_get_cameras(self, mock_home, mock_sess):
"""Check retrieval of camera information."""
mock_home.return_value = {
'cameras': [{'name': 'foo', 'network_id': 1234, 'id': 5678},
{'name': 'bar', 'network_id': 1234, 'id': 5679},
{'name': 'test', 'network_id': 4321, 'id': 0000}]
}
result = self.blink.get_cameras()
self.assertEqual(result, {'1234': [{'name': 'foo', 'id': 5678},
{'name': 'bar', 'id': 5679}],
'4321': [{'name': 'test', 'id': 0000}]})
@mock.patch('blinkpy.api.request_homescreen')
def test_get_cameras_failure(self, mock_home, mock_sess):
"""Check that on failure we initialize empty info and move on."""
mock_home.return_value = {}
result = self.blink.get_cameras()
self.assertEqual(result, {})
+35 -42
View File
@@ -11,7 +11,7 @@ from unittest import mock
from blinkpy import blinkpy
from blinkpy.helpers.util import create_session, BlinkURLHandler
from blinkpy.sync_module import BlinkSyncModule
from blinkpy.camera import BlinkCamera, MAX_CLIPS
from blinkpy.camera import BlinkCamera
import tests.mock_responses as mresp
USERNAME = 'foobar'
@@ -46,46 +46,20 @@ class TestBlinkCameraSetup(unittest.TestCase):
self.blink._auth_header = header
self.blink.session = create_session()
self.blink.urls = BlinkURLHandler('test')
self.blink.sync = BlinkSyncModule(self.blink)
self.camera = BlinkCamera(self.blink.sync)
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', 1234, [])
self.camera = BlinkCamera(self.blink.sync['test'])
self.camera.name = 'foobar'
self.blink.sync.cameras['foobar'] = self.camera
self.blink.sync['test'].cameras['foobar'] = self.camera
def tearDown(self):
"""Clean up after test."""
self.blink = None
def test_check_for_motion(self, mock_sess):
"""Test check for motion function."""
self.assertEqual(self.camera.last_record, [])
self.assertEqual(self.camera.motion_detected, None)
self.camera.sync.record_dates = {'foobar': [1, 3, 2, 4]}
self.camera.check_for_motion()
self.assertEqual(self.camera.last_record, [4])
self.assertEqual(self.camera.motion_detected, False)
self.camera.sync.record_dates = {'foobar': [7, 1, 3, 4]}
self.camera.check_for_motion()
self.assertEqual(self.camera.last_record, [7, 4])
self.assertEqual(self.camera.motion_detected, True)
self.camera.check_for_motion()
self.assertEqual(self.camera.last_record, [7, 4])
self.assertEqual(self.camera.motion_detected, False)
def test_max_motion_clips(self, mock_sess):
"""Test that we only maintain certain number of records."""
for i in range(0, MAX_CLIPS):
self.camera.last_record.append(i)
self.camera.sync.record_dates['foobar'] = [MAX_CLIPS+2]
self.assertEqual(len(self.camera.last_record), MAX_CLIPS)
self.camera.check_for_motion()
self.assertEqual(self.camera.motion_detected, True)
self.assertEqual(len(self.camera.last_record), MAX_CLIPS)
def test_camera_update(self, mock_sess):
"""Test that we can properly update camera properties."""
config = {
'name': 'new',
'camera_id': 1234,
'id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
@@ -96,8 +70,14 @@ class TestBlinkCameraSetup(unittest.TestCase):
'thumbnail': '/thumb',
}
self.camera.last_record = ['1']
self.camera.sync.all_clips = {'new': {'1': '/test.mp4'}}
self.camera.sync.last_record = {
'new': {
'clip': '/test.mp4',
'time': '1970-01-01T00:00:00'
}
}
mock_sess.side_effect = [
mresp.MockResponse({'temp': 71}, 200),
'test',
'foobar'
]
@@ -110,6 +90,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
self.assertEqual(self.camera.battery, 50)
self.assertEqual(self.camera.temperature, 68)
self.assertEqual(self.camera.temperature_c, 20)
self.assertEqual(self.camera.temperature_calibrated, 71)
self.assertEqual(self.camera.wifi_strength, 4)
self.assertEqual(self.camera.thumbnail,
'https://rest.test.immedia-semi.com/thumb.jpg')
@@ -120,13 +101,21 @@ class TestBlinkCameraSetup(unittest.TestCase):
def test_thumbnail_not_in_info(self, mock_sess):
"""Test that we grab thumbanil if not in camera_info."""
mock_sess.side_effect = ['foobar', 'barfoo']
mock_sess.side_effect = [
mresp.MockResponse({'temp': 71}, 200),
'foobar',
'barfoo'
]
self.camera.last_record = ['1']
self.camera.sync.record_dates['new'] = ['1']
self.camera.sync.all_clips = {'new': {'1': '/test.mp4'}}
self.camera.sync.last_record = {
'new': {
'clip': '/test.mp4',
'time': '1970-01-01T00:00:00'
}
}
config = {
'name': 'new',
'camera_id': 1234,
'id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
@@ -153,11 +142,9 @@ class TestBlinkCameraSetup(unittest.TestCase):
"""Tests that thumbnail is 'None' if none found."""
mock_sess.return_value = 'foobar'
self.camera.last_record = ['1']
self.camera.sync.record_dates['new'] = ['1']
self.camera.sync.all_clips = {'new': {'1': '/test.mp4'}}
config = {
'name': 'new',
'camera_id': 1234,
'id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
@@ -170,12 +157,18 @@ class TestBlinkCameraSetup(unittest.TestCase):
self.camera.sync.homescreen = {
'devices': []
}
self.assertEqual(self.camera.temperature_calibrated, None)
with self.assertLogs() as logrecord:
self.camera.update(config)
self.camera.update(config, force=True)
self.assertEqual(self.camera.thumbnail, None)
self.assertEqual(self.camera.last_record, ['1'])
self.assertEqual(self.camera.temperature_calibrated, 68)
self.assertEqual(
logrecord.output,
["ERROR:blinkpy.camera:Could not find thumbnail for camera new"]
[("WARNING:blinkpy.camera:Could not retrieve calibrated "
"temperature."),
("ERROR:blinkpy.camera:Could not find thumbnail for camera new"
"\nNoneType: None")]
)
def test_no_video_clips(self, mock_sess):
@@ -183,7 +176,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
mock_sess.return_value = 'foobar'
config = {
'name': 'new',
'camera_id': 1234,
'id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
+110 -64
View File
@@ -24,77 +24,123 @@ class TestBlinkSyncModule(unittest.TestCase):
'TOKEN_AUTH': 'foobar123'
}
self.blink.urls = blinkpy.BlinkURLHandler('test')
self.blink.sync = BlinkSyncModule(self.blink)
self.blink.sync['test'] = BlinkSyncModule(self.blink,
'test',
'1234',
[])
self.camera = BlinkCamera(self.blink.sync)
def tearDown(self):
"""Clean up after test."""
self.blink = None
self.camera = None
def test_get_events(self, mock_resp):
"""Test get events function."""
mock_resp.return_value = {'event': True}
self.assertEqual(self.blink.sync.get_events(), True)
def test_get_camera_info(self, mock_resp):
"""Test get camera info function."""
mock_resp.return_value = {'devicestatus': True}
self.assertEqual(self.blink.sync.get_camera_info(), True)
def test_get_videos_one_page(self, mock_resp):
"""Test video access."""
mock_resp.return_value = [
{
'camera_name': 'foobar',
'address': '/test/clip_1900_01_01_12_00_00AM.mp4',
'thumbnail': '/test/thumb'
}
]
expected_videos = {'foobar': [
{'clip': '/test/clip_1900_01_01_12_00_00AM.mp4',
'thumb': '/test/thumb'}]}
expected_records = {'foobar': ['1900_01_01_12_00_00AM']}
expected_clips = {'foobar': {
'1900_01_01_12_00_00AM': '/test/clip_1900_01_01_12_00_00AM.mp4'}}
self.blink.sync.get_videos(start_page=0, end_page=0)
self.assertEqual(self.blink.sync.videos, expected_videos)
self.assertEqual(self.blink.sync.record_dates, expected_records)
self.assertEqual(self.blink.sync.all_clips, expected_clips)
def test_get_videos_multi_page(self, mock_resp):
"""Test video access with multiple pages."""
mock_resp.return_value = [
{
'camera_name': 'test',
'address': '/foo/bar_1900_01_01_12_00_00AM.mp4',
'thumbnail': '/foobar'
}
]
self.blink.sync.get_videos()
self.assertEqual(mock_resp.call_count, 2)
mock_resp.reset_mock()
self.blink.sync.get_videos(start_page=0, end_page=9)
self.assertEqual(mock_resp.call_count, 10)
def test_sync_start(self, mock_resp):
"""Test sync start function."""
mock_resp.side_effect = [
self.mock_start = [
{'syncmodule': {
'name': 'test',
'id': 1234,
'network_id': 5678,
'serial': '12345678',
'status': 'foobar'}},
{'event': True},
{},
{'devicestatus': {}},
{},
None,
None
{'devicestatus': {}},
]
self.blink.sync.start()
self.assertEqual(self.blink.sync.name, 'test')
self.assertEqual(self.blink.sync.sync_id, 1234)
self.assertEqual(self.blink.sync.network_id, 5678)
self.assertEqual(self.blink.sync.serial, '12345678')
self.assertEqual(self.blink.sync.status, 'foobar')
def tearDown(self):
"""Clean up after test."""
self.blink = None
self.camera = None
self.mock_start = None
def test_get_events(self, mock_resp):
"""Test get events function."""
mock_resp.return_value = {'event': True}
self.assertEqual(self.blink.sync['test'].get_events(), True)
def test_get_camera_info(self, mock_resp):
"""Test get camera info function."""
mock_resp.return_value = {'camera': ['foobar']}
self.assertEqual(self.blink.sync['test'].get_camera_info('1234'),
'foobar')
def test_check_new_videos(self, mock_resp):
"""Test recent video response."""
mock_resp.return_value = {
'videos': [{
'camera_name': 'foo',
'address': '/foo/bar.mp4',
'created_at': '1970-01-01T00:00:00+0:00'
}]
}
sync_module = self.blink.sync['test']
sync_module.cameras = {'foo': None}
self.assertEqual(sync_module.motion, {})
self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.last_record['foo'],
{'clip': '/foo/bar.mp4',
'time': '1970-01-01T00:00:00+0:00'})
self.assertEqual(sync_module.motion, {'foo': True})
mock_resp.return_value = {'videos': []}
self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': False})
self.assertEqual(sync_module.last_record['foo'],
{'clip': '/foo/bar.mp4',
'time': '1970-01-01T00:00:00+0:00'})
def test_check_new_videos_failed(self, mock_resp):
"""Test method when response is unexpected."""
mock_resp.side_effect = [None, 'just a string', {}]
sync_module = self.blink.sync['test']
sync_module.cameras = {'foo': None}
sync_module.motion['foo'] = True
self.assertFalse(sync_module.check_new_videos())
self.assertFalse(sync_module.motion['foo'])
sync_module.motion['foo'] = True
self.assertFalse(sync_module.check_new_videos())
self.assertFalse(sync_module.motion['foo'])
sync_module.motion['foo'] = True
self.assertFalse(sync_module.check_new_videos())
self.assertFalse(sync_module.motion['foo'])
def test_sync_start(self, mock_resp):
"""Test sync start function."""
mock_resp.side_effect = self.mock_start
self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].name, 'test')
self.assertEqual(self.blink.sync['test'].sync_id, 1234)
self.assertEqual(self.blink.sync['test'].network_id, 5678)
self.assertEqual(self.blink.sync['test'].serial, '12345678')
self.assertEqual(self.blink.sync['test'].status, 'foobar')
def test_unexpected_summary(self, mock_resp):
"""Test unexpected summary response."""
self.mock_start[0] = None
mock_resp.side_effect = self.mock_start
self.assertFalse(self.blink.sync['test'].start())
def test_summary_with_no_network_id(self, mock_resp):
"""Test handling of bad summary."""
self.mock_start[0]['syncmodule'] = None
mock_resp.side_effect = self.mock_start
self.assertFalse(self.blink.sync['test'].start())
def test_summary_with_only_network_id(self, mock_resp):
"""Test handling of sparse summary."""
self.mock_start[0]['syncmodule'] = {'network_id': 8675309}
mock_resp.side_effect = self.mock_start
self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].network_id, 8675309)
def test_unexpected_camera_info(self, mock_resp):
"""Test unexpected camera info response."""
self.blink.sync['test'].cameras['foo'] = None
self.mock_start[5] = None
mock_resp.side_effect = self.mock_start
self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].cameras, {'foo': None})
def test_missing_camera_info(self, mock_resp):
"""Test missing key from camera info response."""
self.blink.sync['test'].cameras['foo'] = None
self.mock_start[5] = {}
self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].cameras, {'foo': None})
+100
View File
@@ -0,0 +1,100 @@
"""Test various api functions."""
import unittest
from unittest import mock
import time
from blinkpy.helpers.util import Throttle
class TestUtil(unittest.TestCase):
"""Test the helpers/util module."""
def setUp(self):
"""Initialize the blink module."""
def tearDown(self):
"""Tear down blink module."""
def test_throttle(self):
"""Test the throttle decorator."""
calls = []
@Throttle(seconds=5)
def test_throttle():
calls.append(1)
now = int(time.time())
now_plus_four = now + 4
now_plus_six = now + 6
test_throttle()
self.assertEqual(1, len(calls))
# Call again, still shouldn't fire
test_throttle()
self.assertEqual(1, len(calls))
# Call with force
test_throttle(force=True)
self.assertEqual(2, len(calls))
# Call without throttle, shouldn't fire
test_throttle()
self.assertEqual(2, len(calls))
# Fake time as 4 seconds from now
with mock.patch('time.time', return_value=now_plus_four):
test_throttle()
self.assertEqual(2, len(calls))
# Fake time as 6 seconds from now
with mock.patch('time.time', return_value=now_plus_six):
test_throttle()
self.assertEqual(3, len(calls))
def test_throttle_per_instance(self):
"""Test that throttle is done once per instance of class."""
class Tester:
"""A tester class for throttling."""
def test(self):
"""Test the throttle."""
return True
tester = Tester()
throttled = Throttle(seconds=1)(tester.test)
self.assertEqual(throttled(), True)
self.assertEqual(throttled(), None)
def test_throttle_on_two_methods(self):
"""Test that throttle works for multiple methods."""
class Tester:
"""A tester class for throttling."""
@Throttle(seconds=3)
def test1(self):
"""Test function for throttle."""
return True
@Throttle(seconds=5)
def test2(self):
"""Test function for throttle."""
return True
tester = Tester()
now = time.time()
now_plus_4 = now + 4
now_plus_6 = now + 6
self.assertEqual(tester.test1(), True)
self.assertEqual(tester.test2(), True)
self.assertEqual(tester.test1(), None)
self.assertEqual(tester.test2(), None)
with mock.patch('time.time', return_value=now_plus_4):
self.assertEqual(tester.test1(), True)
self.assertEqual(tester.test2(), None)
with mock.patch('time.time', return_value=now_plus_6):
self.assertEqual(tester.test1(), None)
self.assertEqual(tester.test2(), True)