Compare commits

...
188 Commits
Author SHA1 Message Date
Kevin FronczakandGitHub 19c9178db7 Merge pull request #223 from fronzbot/fronzbot-patch-1
Update README.rst
2020-04-15 11:51:55 -04:00
Kevin FronczakandGitHub 2ebf2db04d Update README.rst 2020-04-15 11:47:38 -04:00
Kevin FronczakandGitHub 26de89c1b9 Merge pull request #222 from fronzbot/no-camera-enabled-bug
Log response code and message if not trying to re-authorize
2020-04-15 11:26:14 -04:00
Kevin Fronczak 8f68aad238 Create codecov.yml for confugration 2020-04-15 11:21:38 -04:00
Kevin Fronczak 7b586e2947 Log response code and message if not trying to re-authorize (fixes #213) 2020-04-15 11:10:13 -04:00
Kevin FronczakandGitHub 800ddeaf7a Dev version bump 2020-04-15 11:09:46 -04:00
Kevin FronczakandGitHub 4ed2e3dab5 dev version update for pre-release 2020-04-13 20:50:39 -04:00
Kevin FronczakandGitHub 2b9c82b432 Merge pull request #221 from fronzbot/mask-motion-with-arm
Mask motion detection with sync module arm status
2020-04-13 20:48:49 -04:00
Kevin Fronczak 3e7c9f3d5c Mask motion detection with sync module arm status 2020-04-13 20:42:55 -04:00
Kevin FronczakandGitHub 8d0b51ed9c Merge pull request #220 from fronzbot/fix-video-saving
Only store motion clip if motion occurred since last refresh
2020-04-13 20:31:02 -04:00
Kevin Fronczak c178fa5457 Only store motion clip if motion occured since last refresh 2020-04-13 20:02:13 -04:00
Kevin FronczakandGitHub 532ad92714 Merge pull request #219 from fronzbot/json-creds
Load credentials from json file
2020-04-13 19:17:34 -04:00
Kevin Fronczak deb0a682d8 revert tox 2020-04-13 19:05:56 -04:00
Kevin Fronczak 12e22dee8e Load credentials from json file 2020-04-13 18:53:37 -04:00
Kevin FronczakandGitHub ae0f954632 Merge pull request #218 from fronzbot/fix-codecov
Update coverage action
2020-04-13 17:02:40 -04:00
Kevin Fronczak b92941538b Update coverage action 2020-04-13 16:59:53 -04:00
Kevin FronczakandGitHub ce13444427 Merge pull request #217 from fronzbot/bugfix-205
Remove exc_info fixes #205
2020-04-13 16:48:10 -04:00
Kevin FronczakandGitHub 9c461c4cfa Remove exc_info fixes #205 2020-04-13 16:45:36 -04:00
Kevin FronczakandGitHub 23858c2912 Merge pull request #216 from fronzbot/Github-Actions
Migrate to github actions
2020-04-13 16:41:42 -04:00
Kevin FronczakandGitHub 2284a56c82 Delete .travis.yml 2020-04-13 16:37:46 -04:00
Kevin FronczakandGitHub 323143cb2b Fix name 2020-04-13 16:32:50 -04:00
Kevin FronczakandGitHub d6064a9aac Create coverage.yml 2020-04-13 16:32:33 -04:00
Kevin FronczakandGitHub a982aa595f Remove coverage 2020-04-13 16:31:48 -04:00
Kevin FronczakandGitHub 54c52a7f1e Update tests.yml 2020-04-13 16:30:32 -04:00
Kevin FronczakandGitHub bdb8cedf64 Update tests.yml 2020-04-13 16:29:15 -04:00
Kevin FronczakandGitHub 554b18adfe Update README.rst 2020-04-13 16:24:50 -04:00
Kevin FronczakandGitHub 5619af2a00 Update build badge 2020-04-13 16:24:27 -04:00
Kevin FronczakandGitHub 957003aeb1 Rename workflow 2020-04-13 16:22:49 -04:00
Kevin FronczakandGitHub 6fc64a486e Create publish.yml 2020-04-13 16:18:40 -04:00
Kevin FronczakandGitHub 534e3fb34e Reduce to only one VM 2020-04-13 16:14:17 -04:00
Kevin FronczakandGitHub 9ab346ed91 Create tests.yml 2020-04-13 16:11:09 -04:00
Kevin FronczakandGitHub 1bf5dda803 Create lint.yml 2020-04-13 16:02:14 -04:00
Kevin FronczakandGitHub 6cd8fd8ab2 Merge pull request #215 from fronzbot/codecov
Migrate from coveralls to codecov
2020-04-09 15:20:09 -04:00
Kevin Fronczak a689839616 Update codecov badge 2020-04-09 15:16:19 -04:00
Kevin Fronczak 783aa5e547 Migrate from coveralls to codecov 2020-04-09 15:07:30 -04:00
Kevin FronczakandGitHub a6ff2fa0df Merge pull request #214 from fronzbot/bugfix-motion
Fix motion detection getting stuck 'on'
2020-04-09 15:05:44 -04:00
Kevin Fronczak eb498bfb22 Pin coverage in traviswq 2020-04-09 15:01:33 -04:00
Kevin Fronczak 2ebbe46ada Change time conversion to something that supports older python versions 2020-04-09 14:51:06 -04:00
Kevin Fronczak 2f4e8fcade Ignore flake8-black errors 2020-04-09 14:14:47 -04:00
Kevin Fronczak efe05525e4 Fix tests, pin coverage due to sqlite problems 2020-04-09 14:11:19 -04:00
Kevin Fronczak 4122ae8f64 Added time check on latest video 2020-04-08 18:45:59 -04:00
Kevin FronczakandGitHub 5d296cc836 Merge pull request #203 from fronzbot/merge-v0.14.2-into-dev
Merge v0.14.2 into dev
2019-10-12 14:48:45 -04:00
Kevin Fronczak fa50e315a9 dev version bump 2019-10-12 14:42:34 -04:00
Kevin FronczakandGitHub 4a4811bee2 Merge pull request #202 from fronzbot/release-0.14.2
Release 0.14.2
2019-10-12 14:38:35 -04:00
Kevin Fronczak ad7ece4d45 Fixed version.wq 2019-10-12 14:32:26 -04:00
Kevin Fronczak 17d1aa9690 Update changelog, bumped version 2019-10-12 14:31:20 -04:00
Kevin FronczakandGitHub b00839ecb3 Merge pull request #198 from 3ch01c/docker
Add Docker example files
2019-10-12 14:01:25 -04:00
Kevin Fronczak 4900ea8ba6 Add app to tox check 2019-10-12 13:41:57 -04:00
Kevin Fronczak 4fb16398e4 Restructured to allow for future expansion
- Removed docker_compose
2019-10-12 13:27:14 -04:00
Jack Miner a1fc0c5a2b Added TIMEDELTA environment variable to configure how far back to download videos. Wrapped long lines to make linter happy. 2019-09-22 11:43:22 -06:00
Jack Miner 6adb252cdd dockerized 2019-09-14 02:01:44 -06:00
Kevin FronczakandGitHub 3637bbad8d Merge pull request #196 from fronzbot/req
Update python slugify version, better versioning
2019-09-09 11:12:45 -04:00
Kevin FronczakandGitHub 694efe71f8 Update python slugify version, better versioning 2019-09-02 10:29:14 -04:00
Kevin FronczakandGitHub 315735bc2f Merge pull request #192 from fronzbot/dev-version-bump
Dev verison bump
2019-06-20 13:56:16 -04:00
Kevin Fronczak db6403b468 Dev verison bump 2019-06-20 13:44:11 -04:00
Kevin FronczakandGitHub 92dda40e84 Merge pull request #191 from fronzbot/0.14.1
0.14.1
2019-06-20 13:41:22 -04:00
Kevin Fronczak 6c8fc0abce Updated changes 2019-06-20 13:34:59 -04:00
Kevin Fronczak 2d74b12d43 Version bump 2019-06-20 13:32:46 -04:00
Kevin FronczakandGitHub 197695ee40 Merge pull request #190 from fronzbot/dev-version-bump
Dev version bump
2019-06-20 13:24:24 -04:00
Kevin Fronczak 490c42550d Dev version bump
- Added Py3.8 to supported versions
2019-06-20 12:42:01 -04:00
Kevin FronczakandGitHub 7367eb6e87 Merge pull request #187 from fronzbot/upgrade-login
Upgraded login urls to use rest-region subdomain
2019-06-18 10:55:32 -04:00
Kevin FronczakandGitHub 414b647dc8 Merge pull request #189 from fronzbot/timeout
Improved timeout, prevent blocking on startup when unable to login
2019-06-17 11:54:17 -04:00
Kevin FronczakandGitHub 57882bb811 Merge pull request #188 from fronzbot/remove-thumbnail-method
Remove thumbnail from homescreen method
2019-06-17 11:17:15 -04:00
Kevin Fronczak bb76f09f31 Fix dev version (will add to different PR) 2019-06-17 11:15:46 -04:00
Kevin Fronczak 741cf489c8 Fix tests for improved login routine 2019-06-17 11:11:07 -04:00
Kevin Fronczak 7ce332b3ca Improved timeout, prevent blocking on startup when unable to login 2019-06-17 11:07:16 -04:00
Kevin Fronczak 8b7a491c89 Remove thumbnail from homescreen method 2019-06-17 10:20:23 -04:00
Kevin Fronczak 604eebd1b7 Upgraded login urls to use rest-region subdomain 2019-06-17 10:04:34 -04:00
Kevin FronczakandGitHub c2f44b3dfd Merge pull request #183 from fronzbot/dev-version-bump
Dev version bump
2019-05-24 09:38:56 -04:00
Kevin FronczakandGitHub f4480da276 Merge pull request #180 from fronzbot/dev
0.14.0
2019-05-23 16:56:46 -04:00
Kevin Fronczak ccad3dcb0c Dev version bump 2019-05-23 16:53:02 -04:00
Kevin FronczakandGitHub 7767d3af65 Merge pull request #182 from fronzbot/update-changes
Update CHANGES.rst
2019-05-23 16:51:54 -04:00
Kevin FronczakandGitHub 79e699ba78 Update CHANGES.rst 2019-05-23 16:42:12 -04:00
Kevin FronczakandGitHub b28d6f4eb4 Merge pull request #181 from fronzbot/version-bump
Version bump
2019-05-23 16:40:14 -04:00
Kevin FronczakandGitHub d021f48c67 Update CHANGES.rst 2019-05-23 16:15:07 -04:00
Kevin Fronczak 1e71af28dd Version bump 2019-05-23 16:06:56 -04:00
Kevin FronczakandGitHub cb5867d430 Merge pull request #179 from fronzbot/bugfix
Blink instantiation bugfix
2019-05-23 14:45:26 -04:00
Kevin Fronczak bb887936b6 Blink instantiation bugfix 2019-05-23 14:35:27 -04:00
Kevin FronczakandGitHub 720d520398 Merge pull request #175 from fronzbot/update-subdomains
Change subdomain from rest.region to rest-region
2019-05-21 23:56:25 -04:00
Kevin FronczakandGitHub 8fb1116641 Merge branch 'dev' into update-subdomains 2019-05-21 23:37:00 -04:00
Kevin FronczakandGitHub c319762152 Merge pull request #178 from fronzbot/dev-bump
dev version bump
2019-05-21 23:35:25 -04:00
Kevin Fronczak 726d8b8c65 dev version bump 2019-05-21 23:14:26 -04:00
Kevin FronczakandGitHub 683650e2c2 Merge pull request #177 from fronzbot/fix-motion-detect
Fix motion detect
2019-05-21 23:13:15 -04:00
Kevin Fronczak 50b1a35168 Update tests 2019-05-21 22:46:11 -04:00
Kevin Fronczak b88a5feddf Change default interval to one minute 2019-05-21 22:38:49 -04:00
Kevin Fronczak 93472e38de Added in motion interval to allow for looking at historic motion events.
- Good debug utility
- Helps iron out rapid motion events, or missed events due to quick
calls to refresh method
2019-05-21 22:36:46 -04:00
Kevin Fronczak 85c14ede8d Move constants into helpers/constants.py 2019-05-21 22:23:31 -04:00
Kevin Fronczak 58ce109518 Use UTC for time conversions 2019-05-21 22:09:36 -04:00
Kevin Fronczak cf65648293 Add legacy flag to be able to use old 'rest.region' subdomain 2019-05-20 21:15:08 -04:00
Kevin FronczakandGitHub d46e7ed96d Merge pull request #176 from fronzbot/slug
Slugify filenames in video download to ensure OS interoperability
2019-05-20 20:43:28 -04:00
Kevin Fronczak 00395b3825 Slugify filenames in video download to ensure OS interoperability 2019-05-20 11:04:58 -04:00
Kevin FronczakandGitHub f67f0dd6a2 Dev version bump 2019-05-20 00:00:57 -04:00
Kevin FronczakandGitHub d90cd3309f Update README.rst 2019-05-19 18:00:42 -04:00
Kevin Fronczak 130aa8382a Change subdomain from rest.region to rest-region 2019-05-18 18:23:46 -04:00
Kevin FronczakandGitHub e836a6fa58 Merge pull request #174 from fronzbot/fix-battery-voltage
Change battery percentage to state
2019-05-18 18:17:38 -04:00
Kevin Fronczak 92d89b9fe5 Change battery percentage to state 2019-05-18 12:42:01 -04:00
Kevin FronczakandGitHub c10fb432f7 Merge pull request #173 from fronzbot/reduce-throttling
Remove throttling from critical api methods
2019-05-18 12:23:25 -04:00
Kevin Fronczak 37b729b597 Remove throttling from critical api methods 2019-05-18 12:15:57 -04:00
Kevin FronczakandGitHub 749c68bb8a Merge pull request #172 from fronzbot/fix-video-api
Changed log to print
2019-05-18 11:57:46 -04:00
Kevin Fronczak dcf0ce6394 Fix lint issue 2019-05-18 11:51:58 -04:00
Kevin Fronczak 102190e61b Changed log to print 2019-05-18 11:41:17 -04:00
Kevin FronczakandGitHub a7340c97ca Merge pull request #171 from fronzbot/fix-video-api
Added changed video download endpoint
2019-05-18 11:40:23 -04:00
Kevin Fronczak 43c1162634 Added vieo endpoint key changes to motion detect logic 2019-05-18 10:54:24 -04:00
Kevin Fronczak 4db7a33ef3 Added changed video download endpoint 2019-05-18 10:48:58 -04:00
Kevin FronczakandGitHub bfdc1e47bd Dev version bump 2019-03-01 22:00:37 -05:00
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
35 changed files with 1529 additions and 454 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
[run] [run]
omit = parallel = true
helpers/* omit =
tests/* tests/*
setup.py setup.py
+2
View File
@@ -0,0 +1,2 @@
[flake8]
ignore = BLK100
+41
View File
@@ -0,0 +1,41 @@
name: coverage
on:
push:
branches: [ master, dev ]
pull_request:
branches: [ master, dev ]
jobs:
build:
runs-on: ${{ matrix.platform }}
strategy:
max-parallel: 4
matrix:
platform:
- ubuntu-latest
python-version: [3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements_test.txt
pip install tox
pip install codecov
- name: Test
run: |
tox -r -e cov
- name: Codecov
uses: codecov/codecov-action@v1.0.6
with:
flags: unittests
file: ./coverage.xml
name: blinkpy
fail_ci_if_error: true
+34
View File
@@ -0,0 +1,34 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Lint
on:
push:
branches: [ master, dev ]
pull_request:
branches: [ master, dev ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements_test.txt
pip install tox
- name: Lint
run: |
tox -r -e lint
+31
View File
@@ -0,0 +1,31 @@
# This workflows will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: Upload Python Package
on:
release:
types: [created]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: '3.7'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py bdist_wheel
twine upload dist/*
+33
View File
@@ -0,0 +1,33 @@
name: build
on:
push:
branches: [ master, dev ]
pull_request:
branches: [ master, dev ]
jobs:
build:
runs-on: ${{ matrix.platform }}
strategy:
max-parallel: 4
matrix:
platform:
- ubuntu-latest
python-version: [3.5, 3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements_test.txt
pip install tox
- name: Test
run: |
tox -r
+1
View File
@@ -4,6 +4,7 @@
__pycache__/* __pycache__/*
htmlcov/* htmlcov/*
.coverage .coverage
coverage.xml
*.pyc *.pyc
*.egg*/* *.egg*/*
dist/* dist/*
+4
View File
@@ -4,4 +4,8 @@ python:
ignored_directories: ignored_directories:
- docs - docs
flake8:
enabled: true
config_file: .flake8
fail_on_violations: true fail_on_violations: true
-23
View File
@@ -1,23 +0,0 @@
sudo: required
matrix:
fast_finish: true
include:
- python: "3.6"
env: TOXENV=lint
- python: "3.5.3"
env: TOXENV=py35
- python: "3.6"
env: TOXENV=py36
- python: "3.6"
env: TOXENV=build
- python: "3.7"
env: TOXENV=py37
dist: xenial
- python: "3.8-dev"
env: TOXENV=py38
dist: xenial
install: pip install -U tox coveralls
language: python
script: tox
after_success: coveralls
+36 -34
View File
@@ -1,5 +1,5 @@
# BlinkMonitorProtocol # BlinkMonitorProtocol
Unofficial documentation for the Client API of the Blink Wire-Free HD Home Monitoring and Alert System. Unofficial documentation for the Client API of the Blink Wire-Free HD Home Monitoring & Alert System.
Copied from https://github.com/MattTW/BlinkMonitorProtocol 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. 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 ## Login
Client login to the Blink Servers. Client login to the Blink Servers.
@@ -19,20 +16,25 @@ Client login to the Blink Servers.
> "password" : "*your blink password*", > "password" : "*your blink password*",
> "client_specifier" : "iPhone 9.2 | 2.2 | 222", > "client_specifier" : "iPhone 9.2 | 2.2 | 222",
> "email" : "*your blink login/email*" > "email" : "*your blink login/email*"
>}' --compressed https://prod.immedia-semi.com/login >}' --compressed https://rest.prod.immedia-semi.com/login
**Response:** **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:** **Notes:**
The authtoken value is passed in a header in future calls. 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. Obtain information about the Blink networks defined for the logged in user.
**Request:** **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:** **Response:**
JSON response containing information including Network ID and Account ID. 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. Obtain information about the Blink Sync Modules on the given network.
**Request:** **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:** **Response:**
JSON response containing information about the known state of the Sync module, most notably if it is online 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
Arm the given network (start recording/reporting motion events) Arm the given network (start recording/reporting motion events)
**Request:** **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:** **Response:**
JSON response containing information about the arm command request, including the command/request ID 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) Disarm the given network (stop recording/reporting motion events)
**Request:** **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:** **Response:**
JSON response containing information about the disarm command request, including the command/request ID 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 Get status info on the given command
**Request:** **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:** **Response:**
JSON response containing state information of the given command, most notably whether it has completed and was successful. 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 Return information displayed on the home screen of the mobile client
**Request:** **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:** **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. 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:** **Notes:**
Not necessary to as part of issuing arm/disarm commands, but contains good summary info. Not necessary to as part of issuing arm/disarm commands, but contains good summary info.
##Events, thumbnails & video captures ## Events, thumbnails & video captures
**Request** **Request**
Get events for a given network (sync module) -- Need network ID from home 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** **Response**
A json list of evets incluing URL's. Replace the "mp4" with "jpg" extension to get the thumbnail of each clip 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** **Request**
Captures a new thumbnail for a camera 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** **Response**
Command information. Command information.
@@ -152,17 +154,17 @@ Command information.
**Request** **Request**
Captures a new video for a camera 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** **Response**
Command information. Command information.
##Video Information ## Video Information
**Request** **Request**
Get the total number of videos in the system 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** **Response**
JSON response containing the total video count. JSON response containing the total video count.
@@ -170,7 +172,7 @@ JSON response containing the total video count.
**Request** **Request**
Gets a paginated set of video information 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** **Response**
JSON response containing a set of video information, including: camera name, creation time, thumbnail URI, size, length 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** **Request**
Gets information for a specific video by ID 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** **Response**
JSON response containing video information JSON response containing video information
@@ -186,7 +188,7 @@ JSON response containing video information
**Request** **Request**
Gets a list of unwatched videos 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** **Response**
JSON response containing unwatched video information JSON response containing unwatched video information
@@ -194,7 +196,7 @@ JSON response containing unwatched video information
**Request** **Request**
Deletes a video 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** **Response**
Unknown - not tested Unknown - not tested
@@ -202,7 +204,7 @@ Unknown - not tested
**Request** **Request**
Deletes all videos 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** **Response**
Unknown - not tested Unknown - not tested
@@ -212,7 +214,7 @@ Unknown - not tested
**Request** **Request**
Gets a list of cameras 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** **Response**
JSON response containing camera information JSON response containing camera information
@@ -220,7 +222,7 @@ JSON response containing camera information
**Request** **Request**
Gets information for one camera 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** **Response**
JSON response containing camera information JSON response containing camera information
@@ -228,7 +230,7 @@ JSON response containing camera information
**Request** **Request**
Gets camera sensor information 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** **Response**
JSON response containing camera sensor information, such as wifi strength, temperature, and battery level 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** **Request**
Enables motion detection for one camera 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** **Response**
JSON response containing camera information JSON response containing camera information
@@ -244,7 +246,7 @@ JSON response containing camera information
**Request** **Request**
Disables motion detection for one camera 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** **Response**
JSON response containing camera information JSON response containing camera information
@@ -257,7 +259,7 @@ JSON response containing camera information
**Request** **Request**
Gets information about devices that have connected to the blink service 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** **Response**
JSON response containing client information, including: type, name, connection time, user ID 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** **Request**
Gets information about supported regions 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** **Response**
JSON response containing region information JSON response containing region information
@@ -273,7 +275,7 @@ JSON response containing region information
**Request** **Request**
Gets information about system health 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** **Response**
"all ports tested are open" "all ports tested are open"
@@ -281,7 +283,7 @@ Gets information about system health
**Request** **Request**
Gets information about programs 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** **Response**
Unknown. Unknown.
+91 -2
View File
@@ -3,6 +3,95 @@ Changelog
A list of changes between each release A list of changes between each release
0.14.2 (2019-10-12)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Update dependencies
- Dockerize `(@3ch01c <https://github.com/fronzbot/blinkpy/pull/198>__)`
0.14.1 (2019-06-20)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Fix timeout problems blocking blinkpy startup
- Updated login urls using ``rest-region`` subdomain
- Removed deprecated thumbanil recovery from homescreen
0.14.0 (2019-05-23)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**Breaking Changes:**
- ``BlinkCamera.battery`` no longer reports a percentage, instead it returns a string representing the state of the battery.
- Previous logic for calculating percentage was incorrect
- raw battery voltage can be accessed via ``BlinkCamera.battery_voltage``
**Bug Fixes:**
- Updated video endpoint (fixes broken motion detection)
- Removed throttling from critical api methods which prevented proper operation of multi-sync unit setups
- Slugify downloaded video names to allow for OS interoperability
- Added one minute offset (``Blink.motion_interval``) when checking for recent motion to allow time for events to propagate to server prior to refresh call.
**Everything else:**
- Changed all urls to use ``rest-region`` rather than ``rest.region``. Ability to revert to old method is enabled by instantiating ``Blink()`` with the ``legacy_subdomain`` variable set to ``True``.
- Added debug mode to ``blinkpy.download_videos`` routine to simply print the videos prepped for download, rather than actually saving them.
- Use UTC for time conversions, rather than local timezone
0.13.1 (2019-03-01)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Remove throttle decorator from network status request
0.13.0 (2019-03-01)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**Breaking change:**
Wifi status reported in dBm again, instead of bars (which is great). Also, the old ``get_camera_info`` method has changed and requires a ``camera_id`` parameter.
- Adds throttle decorator
- Decorate following functions with 4s throttle (call method with ``force=True`` to override):
- request_network_status
- request_syncmodule
- request_system_arm
- request_system_disarm
- request_sync_events
- request_new_image
- request_new_video
- request_video_count
- request_cameras
- request_camera_info
- request_camera_sensors
- request_motion_detection_enable
- request_motion_detection_disable
- Use the updated homescreen api endpoint to retrieve camera information. The old method to retrieve all cameras at once seems to not exist, and this was the only solution I could figure out and confirm to work.
- Adds throttle decorator to refresh function to prevent too many frequent calls with ``force_cache`` flag set to ``True``. This additional throttle can be overridden with the ``force=True`` argument passed to the refresh function.
- Add ability to cycle through login api endpoints to anticipate future endpoint deprecation
0.12.1 (2019-01-31)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Remove logging improvements since they were incompatible with home-assistant logging
0.12.0 (2019-01-31)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Fix video api endpoint, re-enables motion detection
- Add improved logging capability
- Add download video method
- Prevent blinkpy from failing at setup due to api error
0.11.2 (2019-01-23)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Hotfix to prevent platform from stalling due to API change
- Motion detection and video recovery broken until new API endpoint discovered
0.11.1 (2019-01-02)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- 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) 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>`__) - Use networks endpoint rather than homecreen to retrieve arm/disarm status (`@md-reddevil <https://github.com/fronzbot/blinkpy/pull/119>`__)
@@ -14,7 +103,7 @@ A list of changes between each release
0.10.2 (2018-10-30) 0.10.2 (2018-10-30)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Set minimum required version of the requests library to 2.20.0 due to vulnerability in earlier releases. - Set minimum required version of the requests library to 2.20.0 due to vulnerability in earlier releases.
- When multiple networks detected, changed log level to 'warning' from 'error' - When multiple networks detected, changed log level to ``warning`` from ``error``
0.10.1 (2018-10-18) 0.10.1 (2018-10-18)
@@ -42,7 +131,7 @@ A list of changes between each release
- Use session for http requests - Use session for http requests
**Breaking change:** **Breaking change:**
- Cameras now accessed through sync module Blink.sync.cameras - Cameras now accessed through sync module ``Blink.sync.cameras``
0.8.1 (2018-09-24) 0.8.1 (2018-09-24)
+12
View File
@@ -0,0 +1,12 @@
FROM python:3.7-alpine
LABEL maintainer="Kevin Fronczak <kfronczak@gmail.com>"
VOLUME /media
RUN python -m pip install --upgrade pip
RUN pip3 install blinkpy
COPY app/ .
ENTRYPOINT ["python", "./app.py"]
CMD []
+39 -4
View File
@@ -2,6 +2,10 @@ blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version| |Python Version|
================================================================================ ================================================================================
A Python library for the Blink Camera system A Python library for the Blink Camera system
Like the library? Consider buying me a cup of coffee!
|Donate|
Disclaimer: Disclaimer:
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
Published under the MIT license - See LICENSE file for more details. Published under the MIT license - See LICENSE file for more details.
@@ -51,6 +55,23 @@ The simplest way to use this package from a terminal is to call ``Blink.start()`
If you would like to log in without setting up the cameras or system, you can simply call the ``Blink.login()`` function which will prompt for a username and password and then authenticate with the server. This is useful if you want to avoid use of the ``start()`` function which simply acts as a wrapper for more targeted API methods. If you would like to log in without setting up the cameras or system, you can simply call the ``Blink.login()`` function which will prompt for a username and password and then authenticate with the server. This is useful if you want to avoid use of the ``start()`` function which simply acts as a wrapper for more targeted API methods.
In addition, you can also save your credentials in a json file and initialize Blink with the credential file as follows:
.. code:: python
from blinkpy import blinkpy
blink = blinkpy.Blink(cred_file="path/to/credentials.json")
blink.start()
The credential file must be json formatted with a ``username`` and ``password`` key like follows:
.. code:: json
{
"username": "YOUR USER NAME",
"password": "YOUR PASSWORD"
}
Cameras are instantiated as individual ``BlinkCamera`` classes within a ``BlinkSyncModule`` instance. All of your sync modules are stored within the ``Blink.sync`` dictionary and can be accessed using the name of the sync module as the key (this is the name of your sync module in the Blink App). Cameras are instantiated as individual ``BlinkCamera`` classes within a ``BlinkSyncModule`` instance. All of your sync modules are stored within the ``Blink.sync`` dictionary and can be accessed using the name of the sync module as the key (this is the name of your sync module in the Blink App).
The below code will display cameras and their available attributes: The below code will display cameras and their available attributes:
@@ -84,14 +105,28 @@ The ``blinkpy`` api also allows for saving images and videos to a file and snapp
blink.refresh() # Get new information from server blink.refresh() # Get new information from server
camera.image_to_file('/local/path/for/image.jpg') camera.image_to_file('/local/path/for/image.jpg')
camera.video_to_file('/local/path/for/video.mp4') camera.video_to_file('/local/path/for/video.mp4')
You can also use this library to download all videos from the server. In order to do this, you must specify a ``path``. You may also specifiy a how far back in time to go to retrieve videos via the ``since=`` variable (a simple string such as ``"2017/09/21"`` is sufficient), as well as how many pages to traverse via the ``page=`` variable. Note that by default, the library will search the first ten pages which is sufficient in most use cases. Additionally, you can specidy one or more cameras via the ``camera=`` property. This can be a single string indicating the name of the camera, or a list of camera names. By default, it is set to the string ``'all'`` to grab videos from all cameras.
.. |Build Status| image:: https://travis-ci.org/fronzbot/blinkpy.svg?branch=dev Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34am to the ``/home/blink`` directory:
:target: https://travis-ci.org/fronzbot/blinkpy
.. |Coverage Status| image:: https://coveralls.io/repos/github/fronzbot/blinkpy/badge.svg?branch=dev .. code:: python
:target: https://coveralls.io/github/fronzbot/blinkpy?branch=dev
blink = blinkpy.Blink(username="YOUR USER NAME", password="YOUR PASSWORD")
blink.start()
blink.download_videos('/home/blink', since='2018/07/04 09:34')
.. |Build Status| image:: https://github.com/fronzbot/blinkpy/workflows/build/badge.svg
:target: https://github.com/fronzbot/blinkpy/actions?query=workflow%3Abuild
.. |Coverage Status| image:: https://codecov.io/gh/fronzbot/blinkpy/branch/dev/graph/badge.svg
:target: https://codecov.io/gh/fronzbot/blinkpy
.. |PyPi Version| image:: https://img.shields.io/pypi/v/blinkpy.svg .. |PyPi Version| image:: https://img.shields.io/pypi/v/blinkpy.svg
:target: https://pypi.python.org/pypi/blinkpy :target: https://pypi.python.org/pypi/blinkpy
.. |Docs| image:: https://readthedocs.org/projects/blinkpy/badge/?version=latest .. |Docs| image:: https://readthedocs.org/projects/blinkpy/badge/?version=latest
:target: http://blinkpy.readthedocs.io/en/latest/?badge=latest :target: http://blinkpy.readthedocs.io/en/latest/?badge=latest
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/blinkpy.svg .. |Python Version| image:: https://img.shields.io/pypi/pyversions/blinkpy.svg
:target: https://img.shields.io/pypi/pyversions/blinkpy.svg :target: https://img.shields.io/pypi/pyversions/blinkpy.svg
.. |Donate| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UR6Z2B8GXYUCC
+1
View File
@@ -0,0 +1 @@
"""Python init file for app.py."""
+31
View File
@@ -0,0 +1,31 @@
"""Script to run blinkpy as an app."""
from os import environ
from datetime import datetime, timedelta
from blinkpy import blinkpy
USERNAME = environ.get('USERNAME')
PASSWORD = environ.get('PASSWORD')
TIMEDELTA = timedelta(environ.get('TIMEDELTA', 1))
def get_date():
"""Return now - timedelta for blinkpy."""
return (datetime.now() - TIMEDELTA).isoformat()
def download_videos(blink, save_dir='/media'):
"""Make request to download videos."""
blink.download_videos(save_dir, since=get_date())
def start():
"""Startup blink app."""
blink = blinkpy.Blink(username=USERNAME, password=PASSWORD)
blink.start()
return blink
if __name__ == '__main__':
BLINK = start()
download_videos(BLINK)
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
docker build -t fronzbot/blinkpy:latest ./
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# bash run.sh [username] [password]
if [ "$#" -ne 2 ]; then
echo ""
echo "ERROR: Requries Blink username and password as arguments."
echo "bash run.sh [username] [password]"
echo ""
exit 1
fi
set -ex
USER=fronzbot
IMAGE=blinkpy
CONFIG=$HOME/blinkpy_media
USERNAME=$1
PASSWORD=$2
mkdir -p $CONFIG
result=$(docker images -q $IMAGE)
if [ $result ]; then
docker rm $IMAGE
fi
docker run -it --name ${IMAGE} \
-v $CONFIG:/media \
-e USERNAME=${USERNAME} \
-e PASSWORD=${PASSWORD} \
$USER/$IMAGE \
/bin/bash
+132 -29
View File
@@ -3,14 +3,24 @@
import logging import logging
from json import dumps from json import dumps
import blinkpy.helpers.errors as ERROR import blinkpy.helpers.errors as ERROR
from blinkpy.helpers.util import http_req, BlinkException from blinkpy.helpers.util import http_req, get_time, BlinkException, Throttle
from blinkpy.helpers.constants import DEFAULT_URL from blinkpy.helpers.constants import DEFAULT_URL
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MIN_THROTTLE_TIME = 2
def request_login(blink, url, username, password):
"""Login request.""" def request_login(blink, url, username, password, is_retry=False):
"""
Login request.
:param blink: Blink instance.
:param url: Login url.
:param username: Blink username.
:param password: Blink password.
:param is_retry: Is this part of a re-authorization attempt?
"""
headers = { headers = {
'Host': DEFAULT_URL, 'Host': DEFAULT_URL,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -21,7 +31,7 @@ def request_login(blink, url, username, password):
'client_specifier': 'iPhone 9.2 | 2.2 | 222' 'client_specifier': 'iPhone 9.2 | 2.2 | 222'
}) })
return http_req(blink, url=url, headers=headers, data=data, return http_req(blink, url=url, headers=headers, data=data,
json_resp=False, reqtype='post') json_resp=False, reqtype='post', is_retry=is_retry)
def request_networks(blink): def request_networks(blink):
@@ -31,137 +41,230 @@ def request_networks(blink):
def request_network_status(blink, network): def request_network_status(blink, network):
"""Request network information.""" """
Request network information.
:param blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/network/{}".format(blink.urls.base_url, network) url = "{}/network/{}".format(blink.urls.base_url, network)
return http_get(blink, url) return http_get(blink, url)
def request_syncmodule(blink, network): def request_syncmodule(blink, network):
"""Request sync module info.""" """
Request sync module info.
:param blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/network/{}/syncmodules".format(blink.urls.base_url, network) url = "{}/network/{}/syncmodules".format(blink.urls.base_url, network)
return http_get(blink, url) return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_system_arm(blink, network): def request_system_arm(blink, network):
"""Arm system.""" """
Arm system.
:param blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/network/{}/arm".format(blink.urls.base_url, network) url = "{}/network/{}/arm".format(blink.urls.base_url, network)
return http_post(blink, url) return http_post(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_system_disarm(blink, network): def request_system_disarm(blink, network):
"""Disarm system.""" """
Disarm system.
:param blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/network/{}/disarm".format(blink.urls.base_url, network) url = "{}/network/{}/disarm".format(blink.urls.base_url, network)
return http_post(blink, url) return http_post(blink, url)
def request_command_status(blink, network, command_id): def request_command_status(blink, network, command_id):
"""Request command status.""" """
Request command status.
:param blink: Blink instance.
:param network: Sync module network id.
:param command_id: Command id to check.
"""
url = "{}/network/{}/command/{}".format(blink.urls.base_url, url = "{}/network/{}/command/{}".format(blink.urls.base_url,
network, network,
command_id) command_id)
return http_get(blink, url) return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_homescreen(blink): def request_homescreen(blink):
"""Request homescreen info.""" """Request homescreen info."""
url = "{}/homescreen".format(blink.urls.base_url) url = "{}/api/v3/accounts/{}/homescreen".format(blink.urls.base_url,
blink.account_id)
return http_get(blink, url) return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_sync_events(blink, network): def request_sync_events(blink, network):
"""Request events from sync module.""" """
Request events from sync module.
:param blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/events/network/{}".format(blink.urls.base_url, network) url = "{}/events/network/{}".format(blink.urls.base_url, network)
return http_get(blink, url) return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_new_image(blink, network, camera_id): def request_new_image(blink, network, camera_id):
"""Request to capture new thumbnail for camera.""" """
Request to capture new thumbnail for camera.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request new image from.
"""
url = "{}/network/{}/camera/{}/thumbnail".format(blink.urls.base_url, url = "{}/network/{}/camera/{}/thumbnail".format(blink.urls.base_url,
network, network,
camera_id) camera_id)
return http_post(blink, url) return http_post(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_new_video(blink, network, camera_id): def request_new_video(blink, network, camera_id):
"""Request to capture new video clip.""" """
Request to capture new video clip.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request new video from.
"""
url = "{}/network/{}/camera/{}/clip".format(blink.urls.base_url, url = "{}/network/{}/camera/{}/clip".format(blink.urls.base_url,
network, network,
camera_id) camera_id)
return http_post(blink, url) return http_post(blink, url)
def request_video_count(blink, headers): @Throttle(seconds=MIN_THROTTLE_TIME)
def request_video_count(blink):
"""Request total video count.""" """Request total video count."""
url = "{}/api/v2/videos/count".format(blink.urls.base_url) url = "{}/api/v2/videos/count".format(blink.urls.base_url)
return http_get(blink, url) return http_get(blink, url)
def request_videos(blink, page=0): def request_videos(blink, time=None, page=0):
"""Perform a request for videos.""" """
url = "{}/api/v2/videos/page/{}".format(blink.urls.base_url, page) Perform a request for videos.
:param blink: Blink instance.
:param time: Get videos since this time. In epoch seconds.
:param page: Page number to get videos from.
"""
timestamp = get_time(time)
url = "{}/api/v1/accounts/{}/media/changed?since={}&page={}".format(
blink.urls.base_url, blink.account_id, timestamp, page)
return http_get(blink, url) return http_get(blink, url)
def request_cameras(blink, network): def request_cameras(blink, network):
"""Request all camera information.""" """
Request all camera information.
:param Blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/network/{}/cameras".format(blink.urls.base_url, network) url = "{}/network/{}/cameras".format(blink.urls.base_url, network)
return http_get(blink, url) return http_get(blink, url)
def request_camera_info(blink, network, camera_id): def request_camera_info(blink, network, camera_id):
"""Request camera info for one camera.""" """
url = "{}/network/{}/camera/{}".format(blink.urls.base_url, Request camera info for one camera.
network,
camera_id) :param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request info from.
"""
url = "{}/network/{}/camera/{}/config".format(blink.urls.base_url,
network,
camera_id)
return http_get(blink, url) return http_get(blink, url)
def request_camera_sensors(blink, network, camera_id): def request_camera_sensors(blink, network, camera_id):
"""Request camera sensor info for one camera.""" """
Request camera sensor info for one camera.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request sesnor info from.
"""
url = "{}/network/{}/camera/{}/signals".format(blink.urls.base_url, url = "{}/network/{}/camera/{}/signals".format(blink.urls.base_url,
network, network,
camera_id) camera_id)
return http_get(blink, url) return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_motion_detection_enable(blink, network, camera_id): def request_motion_detection_enable(blink, network, camera_id):
"""Enable motion detection for a camera.""" """
Enable motion detection for a camera.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to enable.
"""
url = "{}/network/{}/camera/{}/enable".format(blink.urls.base_url, url = "{}/network/{}/camera/{}/enable".format(blink.urls.base_url,
network, network,
camera_id) camera_id)
return http_post(blink, url) return http_post(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_motion_detection_disable(blink, network, camera_id): def request_motion_detection_disable(blink, network, camera_id):
"""Disable motion detection for a camera.""" """Disable motion detection for a camera.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to disable.
"""
url = "{}/network/{}/camera/{}/disable".format(blink.urls.base_url, url = "{}/network/{}/camera/{}/disable".format(blink.urls.base_url,
network, network,
camera_id) camera_id)
return http_post(blink, url) return http_post(blink, url)
def http_get(blink, url, stream=False, json=True): def http_get(blink, url, stream=False, json=True, is_retry=False):
""" """
Perform an http get request. Perform an http get request.
:param url: URL to perform get request. :param url: URL to perform get request.
:param stream: Stream response? True/FALSE :param stream: Stream response? True/FALSE
:param json: Return json response? TRUE/False :param json: Return json response? TRUE/False
:param is_retry: Is this part of a re-auth attempt?
""" """
if blink.auth_header is None: if blink.auth_header is None:
raise BlinkException(ERROR.AUTH_TOKEN) raise BlinkException(ERROR.AUTH_TOKEN)
_LOGGER.debug("Making GET request to %s", url) _LOGGER.debug("Making GET request to %s", url)
return http_req(blink, url=url, headers=blink.auth_header, return http_req(blink, url=url, headers=blink.auth_header,
reqtype='get', stream=stream, json_resp=json) reqtype='get', stream=stream, json_resp=json,
is_retry=is_retry)
def http_post(blink, url): def http_post(blink, url, is_retry=False):
""" """
Perform an http post request. Perform an http post request.
:param url: URL to perfom post request. :param url: URL to perfom post request.
:param is_retry: Is this part of a re-auth attempt?
""" """
if blink.auth_header is None: if blink.auth_header is None:
raise BlinkException(ERROR.AUTH_TOKEN) raise BlinkException(ERROR.AUTH_TOKEN)
_LOGGER.debug("Making POST request to %s", url) _LOGGER.debug("Making POST request to %s", url)
return http_req(blink, url=url, headers=blink.auth_header, reqtype='post') return http_req(blink, url=url, headers=blink.auth_header,
reqtype='post', is_retry=is_retry)
+224 -47
View File
@@ -1,49 +1,74 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
blinkpy by Kevin Fronczak - A Blink camera Python library. blinkpy is an unofficial api for the Blink security camera system.
repo url: https://github.com/fronzbot/blinkpy
https://github.com/fronzbot/blinkpy
Original protocol hacking by MattTW : Original protocol hacking by MattTW :
https://github.com/MattTW/BlinkMonitorProtocol https://github.com/MattTW/BlinkMonitorProtocol
Published under the MIT license - See LICENSE file for more details. Published under the MIT license - See LICENSE file for more details.
"Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark "Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark
owned by Immedia Inc., see www.blinkforhome.com for more information. owned by Immedia Inc., see www.blinkforhome.com for more information.
I am in no way affiliated with Blink, nor Immedia Inc. blinkpy is in no way affiliated with Blink, nor Immedia Inc.
""" """
import os.path
import time import time
import getpass import getpass
import logging import logging
import json
from shutil import copyfileobj
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
import blinkpy.helpers.errors as ERROR from dateutil.parser import parse
from slugify import slugify
from blinkpy import api from blinkpy import api
from blinkpy.sync_module import BlinkSyncModule from blinkpy.sync_module import BlinkSyncModule
from blinkpy.helpers import errors as ERROR
from blinkpy.helpers.util import ( from blinkpy.helpers.util import (
create_session, merge_dicts, BlinkURLHandler, create_session, merge_dicts, get_time, BlinkURLHandler,
BlinkAuthenticationException) BlinkAuthenticationException, Throttle)
from blinkpy.helpers.constants import ( from blinkpy.helpers.constants import (
BLINK_URL, LOGIN_URL, LOGIN_BACKUP_URL) BLINK_URL, LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL,
DEFAULT_MOTION_INTERVAL, DEFAULT_REFRESH, MIN_THROTTLE_TIME)
from blinkpy.helpers.constants import __version__
REFRESH_RATE = 30
_LOGGER = logging.getLogger('blinkpy') _LOGGER = logging.getLogger(__name__)
class Blink(): class Blink():
"""Class to initialize communication.""" """Class to initialize communication."""
def __init__(self, username=None, password=None, def __init__(self, username=None, password=None,
refresh_rate=REFRESH_RATE): cred_file=None,
refresh_rate=DEFAULT_REFRESH,
motion_interval=DEFAULT_MOTION_INTERVAL,
legacy_subdomain=False):
""" """
Initialize Blink system. Initialize Blink system.
:param username: Blink username (usually email address) :param username: Blink username (usually email address)
:param password: Blink password :param password: Blink password
:param cred_file: JSON formatted file to store credentials.
If username and password are given, file
is ignored. Otherwise, username and password
are loaded from file.
:param refresh_rate: Refresh rate of blink information. :param refresh_rate: Refresh rate of blink information.
Defaults to 15 (seconds) Defaults to 15 (seconds)
:param motion_interval: How far back to register motion in minutes.
Defaults to last refresh time.
Useful for preventing motion_detected property
from de-asserting too quickly.
:param legacy_subdomain: Set to TRUE to use old 'rest.region'
endpoints (only use if you are having
api issues).
""" """
self._username = username self._username = username
self._password = password self._password = password
self._cred_file = cred_file
self._token = None self._token = None
self._auth_header = None self._auth_header = None
self._host = None self._host = None
@@ -55,10 +80,15 @@ class Blink():
self.region_id = None self.region_id = None
self.last_refresh = None self.last_refresh = None
self.refresh_rate = refresh_rate self.refresh_rate = refresh_rate
self.session = None self.session = create_session()
self.networks = [] self.networks = []
self.cameras = CaseInsensitiveDict({}) self.cameras = CaseInsensitiveDict({})
self.video_list = CaseInsensitiveDict({})
self._login_url = LOGIN_URL self._login_url = LOGIN_URL
self.login_urls = []
self.motion_interval = motion_interval
self.version = __version__
self.legacy = legacy_subdomain
@property @property
def auth_header(self): def auth_header(self):
@@ -73,73 +103,113 @@ class Blink():
Essentially this is just a wrapper function for ease of use. Essentially this is just a wrapper function for ease of use.
""" """
if self._username is None or self._password is None: if self._username is None or self._password is None:
self.login() if not self.login():
else: return
self.get_auth_token() elif not self.get_auth_token():
return
camera_list = self.get_cameras()
networks = self.get_ids() networks = self.get_ids()
for network_name, network_id in networks.items(): for network_name, network_id in networks.items():
sync_module = BlinkSyncModule(self, network_name, network_id) if network_id not in camera_list.keys():
camera_list[network_id] = {}
_LOGGER.warning("No cameras found for %s", network_name)
sync_module = BlinkSyncModule(self,
network_name,
network_id,
camera_list[network_id])
sync_module.start() sync_module.start()
self.sync[network_name] = sync_module self.sync[network_name] = sync_module
self.cameras = self.merge_cameras() self.cameras = self.merge_cameras()
def login(self): def login(self):
"""Prompt user for username and password.""" """Prompt user for username and password."""
self._username = input("Username:") if self._cred_file is not None and os.path.isfile(self._cred_file):
self._password = getpass.getpass("Password:") try:
with open(self._cred_file, 'r') as json_file:
creds = json.load(json_file)
self._username = creds['username']
self._password = creds['password']
except ValueError:
_LOGGER.error("Improperly formated json file %s.",
self._cred_file,
exc_info=True)
return False
except KeyError:
_LOGGER.error("JSON file information incomplete %s.",
exc_info=True)
return False
else:
self._username = input("Username:")
self._password = getpass.getpass("Password:")
if self.get_auth_token(): if self.get_auth_token():
_LOGGER.info("Login successful!") _LOGGER.debug("Login successful!")
return True return True
_LOGGER.warning("Unable to login with %s.", self._username) _LOGGER.warning("Unable to login with %s.", self._username)
return False return False
def get_auth_token(self): def get_auth_token(self, is_retry=False):
"""Retrieve the authentication token from Blink.""" """Retrieve the authentication token from Blink."""
if not isinstance(self._username, str): if not isinstance(self._username, str):
raise BlinkAuthenticationException(ERROR.USERNAME) raise BlinkAuthenticationException(ERROR.USERNAME)
if not isinstance(self._password, str): if not isinstance(self._password, str):
raise BlinkAuthenticationException(ERROR.PASSWORD) raise BlinkAuthenticationException(ERROR.PASSWORD)
login_url = LOGIN_URL self.login_urls = [LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL]
self.session = create_session()
response = api.request_login(self,
login_url,
self._username,
self._password)
if response.status_code == 200: response = self.login_request(is_retry=is_retry)
response = response.json()
(self.region_id, self.region), = response['region'].items() if not response:
else: return False
_LOGGER.debug(
("Received response code %s "
"when authenticating, "
"trying new url"), response.status_code
)
login_url = LOGIN_BACKUP_URL
response = api.request_login(self,
login_url,
self._username,
self._password)
self.region_id = 'piri'
self.region = "UNKNOWN"
self._host = "{}.{}".format(self.region_id, BLINK_URL) self._host = "{}.{}".format(self.region_id, BLINK_URL)
self._token = response['authtoken']['authtoken'] self._token = response['authtoken']['authtoken']
self.networks = response['networks']
self._auth_header = {'Host': self._host, self._auth_header = {'Host': self._host,
'TOKEN_AUTH': self._token} 'TOKEN_AUTH': self._token}
self.networks = response['networks'] self.urls = BlinkURLHandler(self.region_id, legacy=self.legacy)
self.urls = BlinkURLHandler(self.region_id)
self._login_url = login_url
return self._auth_header return self._auth_header
def login_request(self, is_retry=False):
"""Make a login request."""
try:
login_url = self.login_urls.pop(0)
except IndexError:
_LOGGER.error("Could not login to blink servers.")
return False
_LOGGER.info("Attempting login with %s", login_url)
response = api.request_login(self,
login_url,
self._username,
self._password,
is_retry=is_retry)
try:
if response.status_code != 200:
response = self.login_request(is_retry=True)
response = response.json()
(self.region_id, self.region), = response['region'].items()
except AttributeError:
_LOGGER.error("Login API endpoint failed with response %s",
response)
return False
except KeyError:
_LOGGER.warning("Could not extract region info.")
self.region_id = 'piri'
self.region = 'UNKNOWN'
self._login_url = login_url
return response
def get_ids(self): def get_ids(self):
"""Set the network ID and Account ID.""" """Set the network ID and Account ID."""
response = api.request_networks(self) response = api.request_networks(self)
# Look for only onboarded network, flag warning if multiple
# since it's unexpected
all_networks = [] all_networks = []
network_dict = {} network_dict = {}
for network, status in self.networks.items(): for network, status in self.networks.items():
@@ -156,6 +226,26 @@ class Blink():
self.network_ids = all_networks self.network_ids = all_networks
return network_dict return network_dict
def get_cameras(self):
"""Retrieve a camera list for each onboarded network."""
response = api.request_homescreen(self)
try:
all_cameras = {}
for camera in response['cameras']:
camera_network = str(camera['network_id'])
camera_name = camera['name']
camera_id = camera['id']
camera_info = {'name': camera_name, 'id': camera_id}
if camera_network not in all_cameras:
all_cameras[camera_network] = []
all_cameras[camera_network].append(camera_info)
return all_cameras
except KeyError:
_LOGGER.error("Initialization failue. Could not retrieve cameras.")
return {}
@Throttle(seconds=MIN_THROTTLE_TIME)
def refresh(self, force_cache=False): def refresh(self, force_cache=False):
""" """
Perform a system refresh. Perform a system refresh.
@@ -166,6 +256,11 @@ class Blink():
for sync_name, sync_module in self.sync.items(): for sync_name, sync_module in self.sync.items():
_LOGGER.debug("Attempting refresh of sync %s", sync_name) _LOGGER.debug("Attempting refresh of sync %s", sync_name)
sync_module.refresh(force_cache=force_cache) sync_module.refresh(force_cache=force_cache)
if not force_cache:
# Prevents rapid clearing of motion detect property
self.last_refresh = int(time.time())
return True
return False
def check_if_ok_to_update(self): def check_if_ok_to_update(self):
"""Check if it is ok to perform an http request.""" """Check if it is ok to perform an http request."""
@@ -174,7 +269,6 @@ class Blink():
if last_refresh is None: if last_refresh is None:
last_refresh = 0 last_refresh = 0
if current_time >= (last_refresh + self.refresh_rate): if current_time >= (last_refresh + self.refresh_rate):
self.last_refresh = current_time
return True return True
return False return False
@@ -184,3 +278,86 @@ class Blink():
for sync in self.sync: for sync in self.sync:
combined = merge_dicts(combined, self.sync[sync].cameras) combined = merge_dicts(combined, self.sync[sync].cameras)
return combined return combined
def download_videos(self, path, since=None,
camera='all', stop=10, debug=False):
"""
Download all videos from server since specified time.
:param path: Path to write files. /path/<cameraname>_<recorddate>.mp4
:param since: Date and time to get videos from.
Ex: "2018/07/28 12:33:00" to retrieve videos since
July 28th 2018 at 12:33:00
:param camera: Camera name to retrieve. Defaults to "all".
Use a list for multiple cameras.
:param stop: Page to stop on (~25 items per page. Default page 10).
:param debug: Set to TRUE to prevent downloading of items.
Instead of downloading, entries will be printed to log.
"""
if since is None:
since_epochs = self.last_refresh
else:
parsed_datetime = parse(since, fuzzy=True)
since_epochs = parsed_datetime.timestamp()
formatted_date = get_time(time_to_convert=since_epochs)
_LOGGER.info("Retrieving videos since %s", formatted_date)
if not isinstance(camera, list):
camera = [camera]
for page in range(1, stop):
response = api.request_videos(self, time=since_epochs, page=page)
_LOGGER.debug("Processing page %s", page)
try:
result = response['media']
if not result:
raise IndexError
except (KeyError, IndexError):
_LOGGER.info("No videos found on page %s. Exiting.", page)
break
self._parse_downloaded_items(result, camera, path, debug)
def _parse_downloaded_items(self, result, camera, path, debug):
"""Parse downloaded videos."""
for item in result:
try:
created_at = item['created_at']
camera_name = item['device_name']
is_deleted = item['deleted']
address = item['media']
except KeyError:
_LOGGER.info("Missing clip information, skipping...")
continue
if camera_name not in camera and 'all' not in camera:
_LOGGER.debug("Skipping videos for %s.", camera_name)
continue
if is_deleted:
_LOGGER.debug("%s: %s is marked as deleted.",
camera_name,
address)
continue
clip_address = "{}{}".format(self.urls.base_url, address)
filename = "{}-{}".format(camera_name, created_at)
filename = "{}.mp4".format(slugify(filename))
filename = os.path.join(path, filename)
if not debug:
if os.path.isfile(filename):
_LOGGER.info("%s already exists, skipping...", filename)
continue
response = api.http_get(self, url=clip_address,
stream=True, json=False)
with open(filename, 'wb') as vidfile:
copyfileobj(response.raw, vidfile)
_LOGGER.info("Downloaded video to %s", filename)
else:
print(("Camera: {}, Timestamp: {}, "
"Address: {}, Filename: {}").format(
camera_name, created_at, address, filename))
+34 -48
View File
@@ -6,8 +6,6 @@ from blinkpy import api
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MAX_CLIPS = 5
class BlinkCamera(): class BlinkCamera():
"""Class to initialize individual camera.""" """Class to initialize individual camera."""
@@ -24,10 +22,11 @@ class BlinkCamera():
self.battery_voltage = None self.battery_voltage = None
self.clip = None self.clip = None
self.temperature = None self.temperature = None
self.temperature_calibrated = None
self.battery_state = None self.battery_state = None
self.motion_detected = None self.motion_detected = None
self.wifi_strength = None self.wifi_strength = None
self.last_record = [] self.last_record = None
self._cached_image = None self._cached_image = None
self._cached_video = None self._cached_video = None
@@ -40,7 +39,9 @@ class BlinkCamera():
'serial': self.serial, 'serial': self.serial,
'temperature': self.temperature, 'temperature': self.temperature,
'temperature_c': self.temperature_c, 'temperature_c': self.temperature_c,
'temperature_calibrated': self.temperature_calibrated,
'battery': self.battery, 'battery': self.battery,
'battery_voltage': self.battery_voltage,
'thumbnail': self.thumbnail, 'thumbnail': self.thumbnail,
'video': self.clip, 'video': self.clip,
'motion_enabled': self.motion_enabled, 'motion_enabled': self.motion_enabled,
@@ -54,8 +55,8 @@ class BlinkCamera():
@property @property
def battery(self): def battery(self):
"""Return battery level as percentage.""" """Return battery as string."""
return round(self.battery_voltage / 180 * 100) return self.battery_state
@property @property
def temperature_c(self): def temperature_c(self):
@@ -92,10 +93,11 @@ class BlinkCamera():
self.network_id, self.network_id,
self.camera_id) self.camera_id)
def update(self, config, force_cache=False): def update(self, config, force_cache=False, **kwargs):
"""Update camera info.""" """Update camera info."""
# force = kwargs.pop('force', False)
self.name = config['name'] self.name = config['name']
self.camera_id = str(config['camera_id']) self.camera_id = str(config['id'])
self.network_id = str(config['network_id']) self.network_id = str(config['network_id'])
self.serial = config['serial'] self.serial = config['serial']
self.motion_enabled = config['enabled'] self.motion_enabled = config['enabled']
@@ -104,25 +106,41 @@ class BlinkCamera():
self.temperature = config['temperature'] self.temperature = config['temperature']
self.wifi_strength = config['wifi_strength'] 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 # Check if thumbnail exists in config, if not try to
# get it from the homescreen info in teh sync module # get it from the homescreen info in the sync module
# otherwise set it to None and log an error # otherwise set it to None and log an error
new_thumbnail = None new_thumbnail = None
thumb_addr = None
if config['thumbnail']: if config['thumbnail']:
thumb_addr = config['thumbnail'] thumb_addr = config['thumbnail']
else: else:
thumb_addr = self.get_thumb_from_homescreen() _LOGGER.warning("Could not find thumbnail for camera %s",
self.name,
exc_info=True)
if thumb_addr is not None: if thumb_addr is not None:
new_thumbnail = "{}{}.jpg".format(self.sync.urls.base_url, new_thumbnail = "{}{}.jpg".format(self.sync.urls.base_url,
thumb_addr) thumb_addr)
# Check if a new motion clip has been recorded try:
# check_for_motion_method sets motion_detected variable self.motion_detected = self.sync.motion[self.name]
self.check_for_motion() except KeyError:
self.motion_detected = False
clip_addr = None clip_addr = None
if self.last_record: if self.name in self.sync.last_record:
clip_addr = self.sync.all_clips[self.name][self.last_record[0]] clip_addr = self.sync.last_record[self.name]['clip']
self.last_record = self.sync.last_record[self.name]['time']
self.clip = "{}{}".format(self.sync.urls.base_url, self.clip = "{}{}".format(self.sync.urls.base_url,
clip_addr) clip_addr)
@@ -147,25 +165,6 @@ class BlinkCamera():
stream=True, stream=True,
json=False) json=False)
def check_for_motion(self):
"""Check if motion detected.."""
try:
records = sorted(self.sync.record_dates[self.name])
new_clip = records.pop()
if new_clip not in self.last_record and self.last_record:
self.motion_detected = True
self.last_record.insert(0, new_clip)
if len(self.last_record) > MAX_CLIPS:
self.last_record.pop()
elif not self.last_record:
self.last_record.insert(0, new_clip)
self.motion_detected = False
else:
self.motion_detected = False
except KeyError:
_LOGGER.info("Could not extract clip info from camera %s",
self.name)
def image_to_file(self, path): def image_to_file(self, path):
""" """
Write image to file. Write image to file.
@@ -189,21 +188,8 @@ class BlinkCamera():
_LOGGER.debug("Writing video from %s to %s", self.name, path) _LOGGER.debug("Writing video from %s to %s", self.name, path)
response = self._cached_video response = self._cached_video
if response is None: if response is None:
_LOGGER.error("No saved video exist for %s.", self.name) _LOGGER.error("No saved video exist for %s.",
self.name)
return return
with open(path, 'wb') as vidfile: with open(path, 'wb') as vidfile:
copyfileobj(response.raw, vidfile) copyfileobj(response.raw, vidfile)
def get_thumb_from_homescreen(self):
"""Retrieve thumbnail from homescreen."""
for device in self.sync.homescreen['devices']:
try:
device_type = device['device_type']
device_name = device['name']
device_thumb = device['thumbnail']
if device_type == 'camera' and device_name == self.name:
return device_thumb
except KeyError:
pass
_LOGGER.error("Could not find thumbnail for camera %s", self.name)
return None
+17 -6
View File
@@ -3,8 +3,8 @@
import os import os
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 11 MINOR_VERSION = 14
PATCH_VERSION = 0 PATCH_VERSION = '3.dev1'
__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION) __version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
@@ -33,6 +33,7 @@ PROJECT_CLASSIFIERS = [
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Topic :: Home Automation' 'Topic :: Home Automation'
] ]
@@ -45,12 +46,22 @@ PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME)
URLS URLS
''' '''
BLINK_URL = 'immedia-semi.com' BLINK_URL = 'immedia-semi.com'
LOGIN_URL = 'https://prod.' + BLINK_URL + '/login' DEFAULT_URL = "{}.{}".format('rest-prod', BLINK_URL)
LOGIN_BACKUP_URL = 'https://rest.piri/' + BLINK_URL + '/login' BASE_URL = "https://{}".format(DEFAULT_URL)
BASE_URL = 'https://prod.' + BLINK_URL LOGIN_URL = "{}/api/v2/login".format(BASE_URL)
DEFAULT_URL = 'prod.' + BLINK_URL OLD_LOGIN_URL = "{}/login".format(BASE_URL)
LOGIN_BACKUP_URL = "https://{}.{}/login".format('rest-piri', BLINK_URL)
''' '''
Dictionaries Dictionaries
''' '''
ONLINE = {'online': True, 'offline': False} ONLINE = {'online': True, 'offline': False}
'''
OTHER
'''
TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S%z'
DEFAULT_MOTION_INTERVAL = 1
DEFAULT_REFRESH = 30
MIN_THROTTLE_TIME = 2
+2
View File
@@ -11,3 +11,5 @@ AUTH_TOKEN = (
"Authentication header incorrect. Are you sure you received your token?" "Authentication header incorrect. Are you sure you received your token?"
) )
REQUEST = (4, "Cannot perform request (get/post type incorrect)") REQUEST = (4, "Cannot perform request (get/post type incorrect)")
BLINK_ERRORS = [101, 400, 404]
+75 -9
View File
@@ -1,14 +1,36 @@
"""Useful functions for blinkpy.""" """Useful functions for blinkpy."""
import logging import logging
import time
from calendar import timegm
from functools import partial, wraps
from requests import Request, Session, exceptions from requests import Request, Session, exceptions
from blinkpy.helpers.constants import BLINK_URL import dateutil.parser
from blinkpy.helpers.constants import BLINK_URL, TIMESTAMP_FORMAT
import blinkpy.helpers.errors as ERROR import blinkpy.helpers.errors as ERROR
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def time_to_seconds(timestamp):
"""Convert TIMESTAMP_FORMAT time to seconds."""
try:
dtime = dateutil.parser.isoparse(timestamp)
except ValueError:
_LOGGER.error("Incorrect timestamp format for conversion: %s.",
timestamp)
return False
return timegm(dtime.timetuple())
def get_time(time_to_convert=None):
"""Create blink-compatible timestamp."""
if time_to_convert is None:
time_to_convert = time.time()
return time.strftime(TIMESTAMP_FORMAT, time.gmtime(time_to_convert))
def merge_dicts(dict_a, dict_b): def merge_dicts(dict_a, dict_b):
"""Merge two dictionaries into one.""" """Merge two dictionaries into one."""
duplicates = [val for val in dict_a if val in dict_b] duplicates = [val for val in dict_a if val in dict_b]
@@ -19,15 +41,21 @@ def merge_dicts(dict_a, dict_b):
def create_session(): def create_session():
"""Create a session for blink communication.""" """
Create a session for blink communication.
From @ericfrederich via
https://github.com/kennethreitz/requests/issues/2011
"""
sess = Session() sess = Session()
sess.get = partial(sess.get, timeout=5)
return sess return sess
def attempt_reauthorization(blink): def attempt_reauthorization(blink):
"""Attempt to refresh auth token and links.""" """Attempt to refresh auth token and links."""
_LOGGER.info("Auth token expired, attempting reauthorization.") _LOGGER.info("Auth token expired, attempting reauthorization.")
headers = blink.get_auth_token() headers = blink.get_auth_token(is_retry=True)
return headers return headers
@@ -58,14 +86,21 @@ def http_req(blink, url='http://example.com', data=None, headers=None,
try: try:
response = blink.session.send(prepped, stream=stream) response = blink.session.send(prepped, stream=stream)
if json_resp and 'code' in response.json(): if json_resp and 'code' in response.json():
if is_retry: resp_dict = response.json()
code = resp_dict['code']
message = resp_dict['message']
if is_retry and code in ERROR.BLINK_ERRORS:
_LOGGER.error("Cannot obtain new token for server auth.") _LOGGER.error("Cannot obtain new token for server auth.")
return None return None
else: elif code in ERROR.BLINK_ERRORS:
headers = attempt_reauthorization(blink) headers = attempt_reauthorization(blink)
if not headers:
raise exceptions.ConnectionError
return http_req(blink, url=url, data=data, headers=headers, return http_req(blink, url=url, data=data, headers=headers,
reqtype=reqtype, stream=stream, reqtype=reqtype, stream=stream,
json_resp=json_resp, is_retry=True) json_resp=json_resp, is_retry=True)
_LOGGER.warning("Response from server: %s - %s", code, message)
except (exceptions.ConnectionError, exceptions.Timeout): except (exceptions.ConnectionError, exceptions.Timeout):
_LOGGER.info("Cannot connect to server with url %s.", url) _LOGGER.info("Cannot connect to server with url %s.", url)
if not is_retry: if not is_retry:
@@ -96,18 +131,49 @@ class BlinkException(Exception):
class BlinkAuthenticationException(BlinkException): class BlinkAuthenticationException(BlinkException):
"""Class to throw authentication exception.""" """Class to throw authentication exception."""
pass
class BlinkURLHandler(): class BlinkURLHandler():
"""Class that handles Blink URLS.""" """Class that handles Blink URLS."""
def __init__(self, region_id): def __init__(self, region_id, legacy=False):
"""Initialize the urls.""" """Initialize the urls."""
self.base_url = "https://rest.{}.{}".format(region_id, BLINK_URL) self.subdomain = 'rest-{}'.format(region_id)
if legacy:
self.subdomain = 'rest.{}'.format(region_id)
self.base_url = "https://{}.{}".format(self.subdomain, BLINK_URL)
self.home_url = "{}/homescreen".format(self.base_url) self.home_url = "{}/homescreen".format(self.base_url)
self.event_url = "{}/events/network".format(self.base_url) self.event_url = "{}/events/network".format(self.base_url)
self.network_url = "{}/network".format(self.base_url) self.network_url = "{}/network".format(self.base_url)
self.networks_url = "{}/networks".format(self.base_url) self.networks_url = "{}/networks".format(self.base_url)
self.video_url = "{}/api/v2/videos".format(self.base_url) self.video_url = "{}/api/v2/videos".format(self.base_url)
_LOGGER.debug("Setting base url to %s.", self.base_url) _LOGGER.debug("Setting base url to %s.", self.base_url)
class Throttle():
"""Class for throttling api calls."""
def __init__(self, seconds=10):
"""Initialize throttle class."""
self.throttle_time = seconds
self.last_call = 0
def __call__(self, method):
"""Throttle caller method."""
def throttle_method():
"""Call when method is throttled."""
return None
@wraps(method)
def wrapper(*args, **kwargs):
"""Wrap that checks for throttling."""
force = kwargs.pop('force', False)
now = int(time.time())
last_call_delta = now - self.last_call
if force or last_call_delta > self.throttle_time:
result = method(*args, *kwargs)
self.last_call = now
return result
return throttle_method()
return wrapper
+109 -86
View File
@@ -5,6 +5,7 @@ import logging
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
from blinkpy import api from blinkpy import api
from blinkpy.camera import BlinkCamera from blinkpy.camera import BlinkCamera
from blinkpy.helpers.util import time_to_seconds
from blinkpy.helpers.constants import ONLINE from blinkpy.helpers.constants import ONLINE
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -13,7 +14,7 @@ _LOGGER = logging.getLogger(__name__)
class BlinkSyncModule(): class BlinkSyncModule():
"""Class to initialize sync module.""" """Class to initialize sync module."""
def __init__(self, blink, network_name, network_id): def __init__(self, blink, network_name, network_id, camera_list):
""" """
Initialize Blink sync module. Initialize Blink sync module.
@@ -30,13 +31,13 @@ class BlinkSyncModule():
self.sync_id = None self.sync_id = None
self.host = None self.host = None
self.summary = None self.summary = None
self.homescreen = None
self.network_info = None self.network_info = None
self.record_dates = {}
self.videos = {}
self.events = [] self.events = []
self.cameras = CaseInsensitiveDict({}) self.cameras = CaseInsensitiveDict({})
self.all_clips = {} self.motion_interval = blink.motion_interval
self.motion = {}
self.last_record = {}
self.camera_list = camera_list
@property @property
def attributes(self): def attributes(self):
@@ -65,7 +66,10 @@ class BlinkSyncModule():
@property @property
def arm(self): def arm(self):
"""Return status of sync module: armed/disarmed.""" """Return status of sync module: armed/disarmed."""
return self.network_info['network']['armed'] try:
return self.network_info['network']['armed']
except (KeyError, TypeError):
return None
@arm.setter @arm.setter
def arm(self, value): def arm(self, value):
@@ -77,102 +81,121 @@ class BlinkSyncModule():
def start(self): def start(self):
"""Initialize the system.""" """Initialize the system."""
response = api.request_syncmodule(self.blink, self.network_id) response = api.request_syncmodule(self.blink,
self.summary = response['syncmodule'] self.network_id)
self.sync_id = self.summary['id'] try:
self.network_id = self.summary['network_id'] self.summary = response['syncmodule']
self.serial = self.summary['serial'] self.network_id = self.summary['network_id']
self.status = self.summary['status'] except (TypeError, KeyError):
_LOGGER.error(("Could not retrieve sync module information "
"with response: %s"), response, exc_info=True)
return False
self.events = self.get_events() try:
self.sync_id = self.summary['id']
self.homescreen = api.request_homescreen(self.blink) self.serial = self.summary['serial']
self.status = self.summary['status']
except KeyError:
_LOGGER.error("Could not extract some sync module info: %s",
response,
exc_info=True)
self.network_info = api.request_network_status(self.blink, self.network_info = api.request_network_status(self.blink,
self.network_id) self.network_id)
camera_info = self.get_camera_info() self.check_new_videos()
for camera_config in camera_info: try:
name = camera_config['name'] for camera_config in self.camera_list:
self.cameras[name] = BlinkCamera(self) if 'name' not in camera_config:
break
name = camera_config['name']
self.cameras[name] = BlinkCamera(self)
self.motion[name] = False
camera_info = self.get_camera_info(camera_config['id'])
self.cameras[name].update(camera_info,
force_cache=True,
force=True)
except KeyError:
_LOGGER.error("Could not create cameras instances for %s",
self.name,
exc_info=True)
return False
self.videos = self.get_videos() return True
for camera_config in camera_info:
name = camera_config['name']
if name in self.cameras:
self.cameras[name].update(camera_config, force_cache=True)
def get_events(self): def get_events(self, **kwargs):
"""Retrieve events from server.""" """Retrieve events from server."""
response = api.request_sync_events(self.blink, self.network_id) force = kwargs.pop('force', False)
return response['event'] response = api.request_sync_events(self.blink,
self.network_id,
force=force)
try:
return response['event']
except (TypeError, KeyError):
_LOGGER.error("Could not extract events: %s",
response,
exc_info=True)
return False
def get_camera_info(self): def get_camera_info(self, camera_id):
"""Retrieve camera information.""" """Retrieve camera information."""
response = api.request_cameras(self.blink, self.network_id) response = api.request_camera_info(self.blink,
return response['devicestatus'] self.network_id,
camera_id)
try:
return response['camera'][0]
except (TypeError, KeyError):
_LOGGER.error("Could not extract camera info: %s",
response,
exc_info=True)
return []
def refresh(self, force_cache=False): def refresh(self, force_cache=False):
"""Get all blink cameras and pulls their most recent status.""" """Get all blink cameras and pulls their most recent status."""
self.events = self.get_events()
self.videos = self.get_videos()
self.homescreen = api.request_homescreen(self.blink)
self.network_info = api.request_network_status(self.blink, self.network_info = api.request_network_status(self.blink,
self.network_id) self.network_id)
camera_info = self.get_camera_info() self.check_new_videos()
for camera_config in camera_info: for camera_name in self.cameras.keys():
name = camera_config['name'] camera_id = self.cameras[camera_name].camera_id
self.cameras[name].update(camera_config, force_cache=force_cache) camera_info = self.get_camera_info(camera_id)
self.cameras[camera_name].update(camera_info,
force_cache=force_cache)
def get_videos(self, start_page=0, end_page=1): def check_new_videos(self):
""" """Check if new videos since last refresh."""
Retrieve last recorded videos per camera. try:
interval = self.blink.last_refresh - self.motion_interval * 60
except TypeError:
# This is the first start, so refresh hasn't happened yet.
# No need to check for motion.
return False
:param start_page: Page to start reading from on blink servers resp = api.request_videos(self.blink,
(defaults to 0) time=interval,
:param end_page: Page to stop reading from (defaults to 1) page=1)
"""
videos = list()
all_dates = dict()
for page_num in range(start_page, end_page + 1): for camera in self.cameras.keys():
this_page = api.request_videos(self.blink, page=page_num) self.motion[camera] = False
if not this_page:
break
videos.append(this_page)
_LOGGER.debug("Getting videos from page %s through %s",
start_page,
end_page)
for page in videos:
for entry in page:
camera_name = entry['camera_name']
clip_addr = entry['address']
thumb_addr = entry['thumbnail']
clip_date = clip_addr.split('_')[-6:]
clip_date = '_'.join(clip_date)
clip_date = clip_date.split('.')[0]
try:
self.all_clips[camera_name][clip_date] = clip_addr
except KeyError:
self.all_clips[camera_name] = {clip_date: clip_addr}
if camera_name not in all_dates: try:
all_dates[camera_name] = list() info = resp['media']
all_dates[camera_name].append(clip_date) except (KeyError, TypeError):
try: _LOGGER.warning("Could not check for motion. Response: %s", resp)
self.videos[camera_name].append( return False
{
'clip': clip_addr, for entry in info:
'thumb': thumb_addr, try:
} name = entry['device_name']
) clip = entry['media']
except KeyError: timestamp = entry['created_at']
self.videos[camera_name] = [ if self.check_new_video_time(timestamp):
{ self.motion[name] = True and self.arm
'clip': clip_addr, self.last_record[name] = {'clip': clip, 'time': timestamp}
'thumb': thumb_addr, except KeyError:
} _LOGGER.debug("No new videos since last refresh.")
]
self.record_dates = all_dates return True
_LOGGER.debug("Retrieved a total of %s records", len(all_dates))
return self.videos def check_new_video_time(self, timestamp):
"""Check if video has timestamp since last refresh."""
return time_to_seconds(timestamp) > self.blink.last_refresh
+13
View File
@@ -0,0 +1,13 @@
codecov:
branch: dev
coverage:
status:
project:
default:
threshold: 0.02
comment: true
require_ci_to_pass: yes
range: 65..90
round: down
precision: 1
+4
View File
@@ -8,6 +8,8 @@ reports=no
# too-many-* - are not enforced for the sake of readability # too-many-* - are not enforced for the sake of readability
# too-few-* - same as too-many-* # too-few-* - same as too-many-*
# no-else-return - I don't see any reason to enforce this. both forms are readable # no-else-return - I don't see any reason to enforce this. both forms are readable
# no-self-use - stupid and only annoying
# unexpected-keyword-arg - doesn't allow for use of **kwargs, which is dumb
disable= disable=
locally-disabled, locally-disabled,
@@ -23,3 +25,5 @@ disable=
too-many-lines, too-many-lines,
too-few-public-methods, too-few-public-methods,
no-else-return, no-else-return,
no-self-use,
unexpected-keyword-arg,
+3 -1
View File
@@ -1,2 +1,4 @@
requests>=2.20.0 python-dateutil~=2.8.0
requests==2.22.0
python-slugify~=3.0.2
testtools==2.3.0 testtools==2.3.0
+9 -8
View File
@@ -1,10 +1,11 @@
flake8==3.5.0 coverage==4.5.4
flake8-docstrings==1.3.0 flake8==3.7.9
pylint==2.1.1 flake8-docstrings==1.5.0
pydocstyle==2.1.1 pylint==2.4.4
pytest==3.7.1 pydocstyle==5.0.2
pytest-cov>=2.3.1 pytest==5.3.5
pytest-sugar>=0.9.0 pytest-cov==2.8.1
pytest-timeout>=1.0.0 pytest-sugar>=0.9.2
pytest-timeout>=1.3.3
restructuredtext-lint>=1.0.1 restructuredtext-lint>=1.0.1
pygments>=2.2.0 pygments>=2.2.0
-2
View File
@@ -67,5 +67,3 @@ def mocked_session_send(*args, **kwargs):
class MockURLHandler(BlinkURLHandler): class MockURLHandler(BlinkURLHandler):
"""Mocks URL Handler in blinkpy module.""" """Mocks URL Handler in blinkpy module."""
pass
+98 -14
View File
@@ -1,12 +1,11 @@
"""Tests camera and system functions.""" """Tests camera and system functions."""
import unittest import unittest
from unittest import mock from unittest import mock
import logging
from requests import Request
from blinkpy import blinkpy from blinkpy import blinkpy
from blinkpy.sync_module import BlinkSyncModule from blinkpy.sync_module import BlinkSyncModule
from blinkpy.helpers.util import create_session from blinkpy.helpers.util import create_session, get_time
import tests.mock_responses as mresp import tests.mock_responses as mresp
USERNAME = 'foobar' USERNAME = 'foobar'
@@ -56,29 +55,114 @@ class TestBlinkFunctions(unittest.TestCase):
"""Clean up after test.""" """Clean up after test."""
self.blink = None self.blink = None
@mock.patch('blinkpy.blinkpy.api.http_req') @mock.patch('blinkpy.blinkpy.api.request_login')
def test_backup_url(self, req, mock_sess): def test_backup_url(self, req, mock_sess):
"""Test backup login method.""" """Test backup login method."""
fake_req = Request('POST', 'http://wrong.url').prepare() json_resp = {
'authtoken': {'authtoken': 'foobar123'},
'networks': {'1234': {'name': 'foobar', 'onboarded': True}}
}
bad_req = mresp.MockResponse({}, 404)
new_req = mresp.MockResponse(json_resp, 200)
req.side_effect = [ req.side_effect = [
mresp.mocked_session_send(fake_req), bad_req,
{'authtoken': {'authtoken': 'foobar123'}, bad_req,
'networks': {'1234': {'name': 'foobar', 'onboarded': True}}} new_req
] ]
self.blink.get_auth_token() self.blink.login_urls = ['test1', 'test2', 'test3']
self.assertEqual(self.blink.region_id, 'piri') self.blink.login_request()
self.assertEqual(self.blink.region, 'UNKNOWN')
# pylint: disable=protected-access # pylint: disable=protected-access
self.assertEqual(self.blink._token, 'foobar123') self.assertEqual(self.blink._login_url, 'test3')
req.side_effect = [
bad_req,
new_req,
bad_req
]
self.blink.login_urls = ['test1', 'test2', 'test3']
self.blink.login_request()
# pylint: disable=protected-access
self.assertEqual(self.blink._login_url, 'test2')
def test_merge_cameras(self, mock_sess): def test_merge_cameras(self, mock_sess):
"""Test merge camera functionality.""" """Test merge camera functionality."""
first_dict = {'foo': 'bar', 'test': 123} first_dict = {'foo': 'bar', 'test': 123}
next_dict = {'foobar': 456, 'bar': 'foo'} next_dict = {'foobar': 456, 'bar': 'foo'}
self.blink.sync['foo'] = BlinkSyncModule(self.blink, 'foo', 1) self.blink.sync['foo'] = BlinkSyncModule(self.blink, 'foo', 1, [])
self.blink.sync['bar'] = BlinkSyncModule(self.blink, 'bar', 2) self.blink.sync['bar'] = BlinkSyncModule(self.blink, 'bar', 2, [])
self.blink.sync['foo'].cameras = first_dict self.blink.sync['foo'].cameras = first_dict
self.blink.sync['bar'].cameras = next_dict self.blink.sync['bar'].cameras = next_dict
result = self.blink.merge_cameras() result = self.blink.merge_cameras()
expected = {'foo': 'bar', 'test': 123, 'foobar': 456, 'bar': 'foo'} expected = {'foo': 'bar', 'test': 123, 'foobar': 456, 'bar': 'foo'}
self.assertEqual(expected, result) self.assertEqual(expected, result)
@mock.patch('blinkpy.blinkpy.api.request_videos')
def test_download_video_exit(self, mock_req, mock_sess):
"""Test we exit method when provided bad response."""
blink = blinkpy.Blink()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
blink.last_refresh = 0
mock_req.return_value = {}
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"INFO:blinkpy.blinkpy:No videos found on page 1. Exiting."
]
with self.assertLogs() as dl_log:
blink.download_videos('/tmp')
self.assertEqual(dl_log.output, expected_log)
@mock.patch('blinkpy.blinkpy.api.request_videos')
def test_parse_downloaded_items(self, mock_req, mock_sess):
"""Test ability to parse downloaded items list."""
blink = blinkpy.Blink()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
generic_entry = {
'created_at': '1970',
'device_name': 'foo',
'deleted': True,
'media': '/bar.mp4'
}
result = [generic_entry]
mock_req.return_value = {'media': result}
blink.last_refresh = 0
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"DEBUG:blinkpy.blinkpy:foo: /bar.mp4 is marked as deleted."
]
with self.assertLogs() as dl_log:
blink.download_videos('/tmp', stop=2)
self.assertEqual(dl_log.output, expected_log)
@mock.patch('blinkpy.blinkpy.api.request_videos')
def test_parse_camera_not_in_list(self, mock_req, mock_sess):
"""Test ability to parse downloaded items list."""
blink = blinkpy.Blink()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
generic_entry = {
'created_at': '1970',
'device_name': 'foo',
'deleted': True,
'media': '/bar.mp4'
}
result = [generic_entry]
mock_req.return_value = {'media': result}
blink.last_refresh = 0
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"DEBUG:blinkpy.blinkpy:Skipping videos for foo."
]
with self.assertLogs() as dl_log:
blink.download_videos('/tmp', camera='bar', stop=2)
self.assertEqual(dl_log.output, expected_log)
+81 -5
View File
@@ -14,6 +14,7 @@ from blinkpy.sync_module import BlinkSyncModule
from blinkpy.helpers.util import ( from blinkpy.helpers.util import (
http_req, create_session, BlinkAuthenticationException, http_req, create_session, BlinkAuthenticationException,
BlinkException, BlinkURLHandler) BlinkException, BlinkURLHandler)
from blinkpy.helpers.constants import __version__
import tests.mock_responses as mresp import tests.mock_responses as mresp
USERNAME = 'foobar' USERNAME = 'foobar'
@@ -30,7 +31,10 @@ class TestBlinkSetup(unittest.TestCase):
self.blink_no_cred = Blink() self.blink_no_cred = Blink()
self.blink = Blink(username=USERNAME, self.blink = Blink(username=USERNAME,
password=PASSWORD) password=PASSWORD)
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', '1234') self.blink.sync['test'] = BlinkSyncModule(self.blink,
'test',
'1234',
[])
self.blink.urls = BlinkURLHandler('test') self.blink.urls = BlinkURLHandler('test')
self.blink.session = create_session() self.blink.session = create_session()
@@ -41,6 +45,7 @@ class TestBlinkSetup(unittest.TestCase):
def test_initialization(self, mock_sess): def test_initialization(self, mock_sess):
"""Verify we can initialize blink.""" """Verify we can initialize blink."""
self.assertEqual(self.blink.version, __version__)
# pylint: disable=protected-access # pylint: disable=protected-access
self.assertEqual(self.blink._username, USERNAME) self.assertEqual(self.blink._username, USERNAME)
# pylint: disable=protected-access # pylint: disable=protected-access
@@ -74,11 +79,52 @@ class TestBlinkSetup(unittest.TestCase):
# pylint: disable=protected-access # pylint: disable=protected-access
self.assertEqual(self.blink_no_cred._password, PASSWORD) self.assertEqual(self.blink_no_cred._password, PASSWORD)
@mock.patch('blinkpy.blinkpy.getpass.getpass')
@mock.patch('blinkpy.blinkpy.Blink.get_auth_token')
def test_no_cred_file(self, getpwd, getauth, mock_sess):
"""Check that normal login occurs when cred file doesn't exist."""
# pylint: disable=protected-access
self.blink._cred_file = '/tmp/fake.file'
getpwd.return_value = PASSWORD
getauth.return_value = True
with mock.patch('builtins.input', return_value=USERNAME):
self.assertTrue(self.blink.login())
def test_exit_on_missing_json(self, mock_sess):
"""Test that we fail on missing json data."""
# pylint: disable=protected-access
self.blink._cred_file = '/tmp/fake.file'
with mock.patch('os.path.isfile', return_value=True):
with mock.patch('builtins.open', mock.mock_open(read_data="{}")):
self.assertFalse(self.blink.login())
def test_exit_on_bad_json(self, mock_sess):
"""Test that we fail on bad json format."""
# pylint: disable=protected-access
self.blink._cred_file = '/tmp/fake.file'
with mock.patch('os.path.isfile', return_value=True):
with mock.patch('builtins.open', mock.mock_open(read_data='{]')):
self.assertFalse(self.blink.login())
@mock.patch('blinkpy.blinkpy.json.load')
def test_cred_file(self, mockjson, mock_sess):
"""Test that loading credential file works."""
# pylint: disable=protected-access
self.blink_no_cred._cred_file = '/tmp/fake.file'
mockjson.return_value = {'username': 'foo', 'password': 'bar'}
with mock.patch('os.path.isfile', return_value=True):
with mock.patch('builtins.open', mock.mock_open(read_data='')):
self.assertTrue(self.blink_no_cred.login())
# pylint: disable=protected-access
self.assertEqual(self.blink_no_cred._username, 'foo')
# pylint: disable=protected-access
self.assertEqual(self.blink_no_cred._password, 'bar')
def test_bad_request(self, mock_sess): def test_bad_request(self, mock_sess):
"""Check that we raise an Exception with a bad request.""" """Check that we raise an Exception with a bad request."""
self.blink.session = create_session() self.blink.session = create_session()
explog = ("ERROR:blinkpy.helpers.util:" explog = ("WARNING:blinkpy.helpers.util:"
"Cannot obtain new token for server auth.") "Response from server: 200 - foo")
with self.assertRaises(BlinkException): with self.assertRaises(BlinkException):
http_req(self.blink, reqtype='bad') http_req(self.blink, reqtype='bad')
@@ -138,9 +184,13 @@ class TestBlinkSetup(unittest.TestCase):
now = self.blink.refresh_rate + 1 now = self.blink.refresh_rate + 1
mock_time.return_value = now mock_time.return_value = now
self.assertEqual(self.blink.last_refresh, None) self.assertEqual(self.blink.last_refresh, None)
result = self.blink.check_if_ok_to_update() self.assertEqual(self.blink.check_if_ok_to_update(), True)
self.assertEqual(self.blink.last_refresh, None)
with mock.patch('blinkpy.sync_module.BlinkSyncModule.refresh',
return_value=True):
self.blink.refresh()
self.assertEqual(self.blink.last_refresh, now) self.assertEqual(self.blink.last_refresh, now)
self.assertEqual(result, True)
self.assertEqual(self.blink.check_if_ok_to_update(), False) self.assertEqual(self.blink.check_if_ok_to_update(), False)
self.assertEqual(self.blink.last_refresh, now) self.assertEqual(self.blink.last_refresh, now)
@@ -148,3 +198,29 @@ class TestBlinkSetup(unittest.TestCase):
"""Check that we can access sync modules ignoring case.""" """Check that we can access sync modules ignoring case."""
self.assertEqual(self.blink.sync['test'].name, 'test') self.assertEqual(self.blink.sync['test'].name, 'test')
self.assertEqual(self.blink.sync['TEST'].name, 'test') self.assertEqual(self.blink.sync['TEST'].name, 'test')
@mock.patch('blinkpy.api.request_login')
def test_unexpected_login(self, mock_login, mock_sess):
"""Check that we appropriately handle unexpected login info."""
mock_login.return_value = None
self.assertFalse(self.blink.get_auth_token())
@mock.patch('blinkpy.api.request_homescreen')
def test_get_cameras(self, mock_home, mock_sess):
"""Check retrieval of camera information."""
mock_home.return_value = {
'cameras': [{'name': 'foo', 'network_id': 1234, 'id': 5678},
{'name': 'bar', 'network_id': 1234, 'id': 5679},
{'name': 'test', 'network_id': 4321, 'id': 0000}]
}
result = self.blink.get_cameras()
self.assertEqual(result, {'1234': [{'name': 'foo', 'id': 5678},
{'name': 'bar', 'id': 5679}],
'4321': [{'name': 'test', 'id': 0000}]})
@mock.patch('blinkpy.api.request_homescreen')
def test_get_cameras_failure(self, mock_home, mock_sess):
"""Check that on failure we initialize empty info and move on."""
mock_home.return_value = {}
result = self.blink.get_cameras()
self.assertEqual(result, {})
+24 -70
View File
@@ -11,7 +11,7 @@ from unittest import mock
from blinkpy import blinkpy from blinkpy import blinkpy
from blinkpy.helpers.util import create_session, BlinkURLHandler from blinkpy.helpers.util import create_session, BlinkURLHandler
from blinkpy.sync_module import BlinkSyncModule from blinkpy.sync_module import BlinkSyncModule
from blinkpy.camera import BlinkCamera, MAX_CLIPS from blinkpy.camera import BlinkCamera
import tests.mock_responses as mresp import tests.mock_responses as mresp
USERNAME = 'foobar' USERNAME = 'foobar'
@@ -46,7 +46,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
self.blink._auth_header = header self.blink._auth_header = header
self.blink.session = create_session() self.blink.session = create_session()
self.blink.urls = BlinkURLHandler('test') self.blink.urls = BlinkURLHandler('test')
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', 1234) self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', 1234, [])
self.camera = BlinkCamera(self.blink.sync['test']) self.camera = BlinkCamera(self.blink.sync['test'])
self.camera.name = 'foobar' self.camera.name = 'foobar'
self.blink.sync['test'].cameras['foobar'] = self.camera self.blink.sync['test'].cameras['foobar'] = self.camera
@@ -55,37 +55,11 @@ class TestBlinkCameraSetup(unittest.TestCase):
"""Clean up after test.""" """Clean up after test."""
self.blink = None self.blink = None
def test_check_for_motion(self, mock_sess):
"""Test check for motion function."""
self.assertEqual(self.camera.last_record, [])
self.assertEqual(self.camera.motion_detected, None)
self.camera.sync.record_dates = {'foobar': [1, 3, 2, 4]}
self.camera.check_for_motion()
self.assertEqual(self.camera.last_record, [4])
self.assertEqual(self.camera.motion_detected, False)
self.camera.sync.record_dates = {'foobar': [7, 1, 3, 4]}
self.camera.check_for_motion()
self.assertEqual(self.camera.last_record, [7, 4])
self.assertEqual(self.camera.motion_detected, True)
self.camera.check_for_motion()
self.assertEqual(self.camera.last_record, [7, 4])
self.assertEqual(self.camera.motion_detected, False)
def test_max_motion_clips(self, mock_sess):
"""Test that we only maintain certain number of records."""
for i in range(0, MAX_CLIPS):
self.camera.last_record.append(i)
self.camera.sync.record_dates['foobar'] = [MAX_CLIPS+2]
self.assertEqual(len(self.camera.last_record), MAX_CLIPS)
self.camera.check_for_motion()
self.assertEqual(self.camera.motion_detected, True)
self.assertEqual(len(self.camera.last_record), MAX_CLIPS)
def test_camera_update(self, mock_sess): def test_camera_update(self, mock_sess):
"""Test that we can properly update camera properties.""" """Test that we can properly update camera properties."""
config = { config = {
'name': 'new', 'name': 'new',
'camera_id': 1234, 'id': 1234,
'network_id': 5678, 'network_id': 5678,
'serial': '12345678', 'serial': '12345678',
'enabled': False, 'enabled': False,
@@ -96,8 +70,14 @@ class TestBlinkCameraSetup(unittest.TestCase):
'thumbnail': '/thumb', 'thumbnail': '/thumb',
} }
self.camera.last_record = ['1'] self.camera.last_record = ['1']
self.camera.sync.all_clips = {'new': {'1': '/test.mp4'}} self.camera.sync.last_record = {
'new': {
'clip': '/test.mp4',
'time': '1970-01-01T00:00:00'
}
}
mock_sess.side_effect = [ mock_sess.side_effect = [
mresp.MockResponse({'temp': 71}, 200),
'test', 'test',
'foobar' 'foobar'
] ]
@@ -107,57 +87,25 @@ class TestBlinkCameraSetup(unittest.TestCase):
self.assertEqual(self.camera.network_id, '5678') self.assertEqual(self.camera.network_id, '5678')
self.assertEqual(self.camera.serial, '12345678') self.assertEqual(self.camera.serial, '12345678')
self.assertEqual(self.camera.motion_enabled, False) self.assertEqual(self.camera.motion_enabled, False)
self.assertEqual(self.camera.battery, 50) self.assertEqual(self.camera.battery, 'ok')
self.assertEqual(self.camera.temperature, 68) self.assertEqual(self.camera.temperature, 68)
self.assertEqual(self.camera.temperature_c, 20) self.assertEqual(self.camera.temperature_c, 20)
self.assertEqual(self.camera.temperature_calibrated, 71)
self.assertEqual(self.camera.wifi_strength, 4) self.assertEqual(self.camera.wifi_strength, 4)
self.assertEqual(self.camera.thumbnail, self.assertEqual(self.camera.thumbnail,
'https://rest.test.immedia-semi.com/thumb.jpg') 'https://rest-test.immedia-semi.com/thumb.jpg')
self.assertEqual(self.camera.clip, self.assertEqual(self.camera.clip,
'https://rest.test.immedia-semi.com/test.mp4') 'https://rest-test.immedia-semi.com/test.mp4')
self.assertEqual(self.camera.image_from_cache, 'test') self.assertEqual(self.camera.image_from_cache, 'test')
self.assertEqual(self.camera.video_from_cache, 'foobar') self.assertEqual(self.camera.video_from_cache, 'foobar')
def test_thumbnail_not_in_info(self, mock_sess):
"""Test that we grab thumbanil if not in camera_info."""
mock_sess.side_effect = ['foobar', 'barfoo']
self.camera.last_record = ['1']
self.camera.sync.record_dates['new'] = ['1']
self.camera.sync.all_clips = {'new': {'1': '/test.mp4'}}
config = {
'name': 'new',
'camera_id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
'battery_voltage': 90,
'battery_state': 'ok',
'temperature': 68,
'wifi_strength': 4,
'thumbnail': '',
}
self.camera.sync.homescreen = {
'devices': [
{'foo': 'bar'},
{'device_type': 'foobar'},
{'device_type': 'camera',
'name': 'new',
'thumbnail': '/new/thumb'}
]
}
self.camera.update(config)
self.assertEqual(self.camera.thumbnail,
'https://rest.test.immedia-semi.com/new/thumb.jpg')
def test_no_thumbnails(self, mock_sess): def test_no_thumbnails(self, mock_sess):
"""Tests that thumbnail is 'None' if none found.""" """Tests that thumbnail is 'None' if none found."""
mock_sess.return_value = 'foobar' mock_sess.return_value = 'foobar'
self.camera.last_record = ['1'] self.camera.last_record = ['1']
self.camera.sync.record_dates['new'] = ['1']
self.camera.sync.all_clips = {'new': {'1': '/test.mp4'}}
config = { config = {
'name': 'new', 'name': 'new',
'camera_id': 1234, 'id': 1234,
'network_id': 5678, 'network_id': 5678,
'serial': '12345678', 'serial': '12345678',
'enabled': False, 'enabled': False,
@@ -170,12 +118,18 @@ class TestBlinkCameraSetup(unittest.TestCase):
self.camera.sync.homescreen = { self.camera.sync.homescreen = {
'devices': [] 'devices': []
} }
self.assertEqual(self.camera.temperature_calibrated, None)
with self.assertLogs() as logrecord: with self.assertLogs() as logrecord:
self.camera.update(config) self.camera.update(config, force=True)
self.assertEqual(self.camera.thumbnail, None) self.assertEqual(self.camera.thumbnail, None)
self.assertEqual(self.camera.last_record, ['1'])
self.assertEqual(self.camera.temperature_calibrated, 68)
self.assertEqual( self.assertEqual(
logrecord.output, logrecord.output,
["ERROR:blinkpy.camera:Could not find thumbnail for camera new"] [("WARNING:blinkpy.camera:Could not retrieve calibrated "
"temperature."),
("WARNING:blinkpy.camera:Could not find thumbnail for camera new"
"\nNoneType: None")]
) )
def test_no_video_clips(self, mock_sess): def test_no_video_clips(self, mock_sess):
@@ -183,7 +137,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
mock_sess.return_value = 'foobar' mock_sess.return_value = 'foobar'
config = { config = {
'name': 'new', 'name': 'new',
'camera_id': 1234, 'id': 1234,
'network_id': 5678, 'network_id': 5678,
'serial': '12345678', 'serial': '12345678',
'enabled': False, 'enabled': False,
+183 -58
View File
@@ -17,69 +17,21 @@ class TestBlinkSyncModule(unittest.TestCase):
def setUp(self): def setUp(self):
"""Set up Blink module.""" """Set up Blink module."""
self.blink = blinkpy.Blink(username=USERNAME, self.blink = blinkpy.Blink(username=USERNAME,
password=PASSWORD) password=PASSWORD,
motion_interval=0)
# pylint: disable=protected-access # pylint: disable=protected-access
self.blink._auth_header = { self.blink._auth_header = {
'Host': 'test.url.tld', 'Host': 'test.url.tld',
'TOKEN_AUTH': 'foobar123' 'TOKEN_AUTH': 'foobar123'
} }
self.blink.last_refresh = 0
self.blink.urls = blinkpy.BlinkURLHandler('test') self.blink.urls = blinkpy.BlinkURLHandler('test')
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', '1234') self.blink.sync['test'] = BlinkSyncModule(self.blink,
'test',
'1234',
[])
self.camera = BlinkCamera(self.blink.sync) self.camera = BlinkCamera(self.blink.sync)
self.mock_start = [
def tearDown(self):
"""Clean up after test."""
self.blink = None
self.camera = None
def test_get_events(self, mock_resp):
"""Test get events function."""
mock_resp.return_value = {'event': True}
self.assertEqual(self.blink.sync['test'].get_events(), True)
def test_get_camera_info(self, mock_resp):
"""Test get camera info function."""
mock_resp.return_value = {'devicestatus': True}
self.assertEqual(self.blink.sync['test'].get_camera_info(), True)
def test_get_videos_one_page(self, mock_resp):
"""Test video access."""
mock_resp.return_value = [
{
'camera_name': 'foobar',
'address': '/test/clip_1900_01_01_12_00_00AM.mp4',
'thumbnail': '/test/thumb'
}
]
expected_videos = {'foobar': [
{'clip': '/test/clip_1900_01_01_12_00_00AM.mp4',
'thumb': '/test/thumb'}]}
expected_recs = {'foobar': ['1900_01_01_12_00_00AM']}
expected_clips = {'foobar': {
'1900_01_01_12_00_00AM': '/test/clip_1900_01_01_12_00_00AM.mp4'}}
self.blink.sync['test'].get_videos(start_page=0, end_page=0)
self.assertEqual(self.blink.sync['test'].videos, expected_videos)
self.assertEqual(self.blink.sync['test'].record_dates, expected_recs)
self.assertEqual(self.blink.sync['test'].all_clips, expected_clips)
def test_get_videos_multi_page(self, mock_resp):
"""Test video access with multiple pages."""
mock_resp.return_value = [
{
'camera_name': 'test',
'address': '/foo/bar_1900_01_01_12_00_00AM.mp4',
'thumbnail': '/foobar'
}
]
self.blink.sync['test'].get_videos()
self.assertEqual(mock_resp.call_count, 2)
mock_resp.reset_mock()
self.blink.sync['test'].get_videos(start_page=0, end_page=9)
self.assertEqual(mock_resp.call_count, 10)
def test_sync_start(self, mock_resp):
"""Test sync start function."""
mock_resp.side_effect = [
{'syncmodule': { {'syncmodule': {
'id': 1234, 'id': 1234,
'network_id': 5678, 'network_id': 5678,
@@ -88,13 +40,186 @@ class TestBlinkSyncModule(unittest.TestCase):
{'event': True}, {'event': True},
{}, {},
{}, {},
{'devicestatus': {}},
None, None,
None {'devicestatus': {}},
] ]
self.blink.sync['test'].network_info = {'network': {'armed': True}}
def tearDown(self):
"""Clean up after test."""
self.blink = None
self.camera = None
self.mock_start = None
def test_get_events(self, mock_resp):
"""Test get events function."""
mock_resp.return_value = {'event': True}
self.assertEqual(self.blink.sync['test'].get_events(), True)
def test_get_camera_info(self, mock_resp):
"""Test get camera info function."""
mock_resp.return_value = {'camera': ['foobar']}
self.assertEqual(self.blink.sync['test'].get_camera_info('1234'),
'foobar')
def test_check_new_videos_startup(self, mock_resp):
"""Test that check_new_videos does not block startup."""
sync_module = self.blink.sync['test']
self.blink.last_refresh = None
self.assertFalse(sync_module.check_new_videos())
def test_check_new_videos(self, mock_resp):
"""Test recent video response."""
mock_resp.return_value = {
'media': [{
'device_name': 'foo',
'media': '/foo/bar.mp4',
'created_at': '1990-01-01T00:00:00+00:00'
}]
}
sync_module = self.blink.sync['test']
sync_module.cameras = {'foo': None}
sync_module.blink.last_refresh = 0
self.assertEqual(sync_module.motion, {})
self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.last_record['foo'],
{'clip': '/foo/bar.mp4',
'time': '1990-01-01T00:00:00+00:00'})
self.assertEqual(sync_module.motion, {'foo': True})
mock_resp.return_value = {'media': []}
self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': False})
self.assertEqual(sync_module.last_record['foo'],
{'clip': '/foo/bar.mp4',
'time': '1990-01-01T00:00:00+00:00'})
def test_check_new_videos_old_date(self, mock_resp):
"""Test videos return response with old date."""
mock_resp.return_value = {
'media': [{
'device_name': 'foo',
'media': '/foo/bar.mp4',
'created_at': '1970-01-01T00:00:00+00:00'
}]
}
sync_module = self.blink.sync['test']
sync_module.cameras = {'foo': None}
sync_module.blink.last_refresh = 1000
self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': False})
def test_check_no_motion_if_not_armed(self, mock_resp):
"""Test that motion detection is not set if module unarmed."""
mock_resp.return_value = {
'media': [{
'device_name': 'foo',
'media': '/foo/bar.mp4',
'created_at': '1990-01-01T00:00:00+00:00'
}]
}
sync_module = self.blink.sync['test']
sync_module.cameras = {'foo': None}
sync_module.blink.last_refresh = 1000
self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': True})
sync_module.network_info = {'network': {'armed': False}}
self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': False})
def test_check_multiple_videos(self, mock_resp):
"""Test motion found even with multiple videos."""
mock_resp.return_value = {
'media': [
{
'device_name': 'foo',
'media': '/foo/bar.mp4',
'created_at': '1970-01-01T00:00:00+00:00'
},
{
'device_name': 'foo',
'media': '/bar/foo.mp4',
'created_at': '1990-01-01T00:00:00+00:00'
},
{
'device_name': 'foo',
'media': '/foobar.mp4',
'created_at': '1970-01-01T00:00:01+00:00'
}
]
}
sync_module = self.blink.sync['test']
sync_module.cameras = {'foo': None}
sync_module.blink.last_refresh = 1000
self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': True})
expected_result = {
'foo': {
'clip': '/bar/foo.mp4',
'time': '1990-01-01T00:00:00+00:00'
}
}
self.assertEqual(sync_module.last_record, expected_result)
def test_check_new_videos_failed(self, mock_resp):
"""Test method when response is unexpected."""
mock_resp.side_effect = [None, 'just a string', {}]
sync_module = self.blink.sync['test']
sync_module.cameras = {'foo': None}
sync_module.motion['foo'] = True
self.assertFalse(sync_module.check_new_videos())
self.assertFalse(sync_module.motion['foo'])
sync_module.motion['foo'] = True
self.assertFalse(sync_module.check_new_videos())
self.assertFalse(sync_module.motion['foo'])
sync_module.motion['foo'] = True
self.assertFalse(sync_module.check_new_videos())
self.assertFalse(sync_module.motion['foo'])
def test_sync_start(self, mock_resp):
"""Test sync start function."""
mock_resp.side_effect = self.mock_start
self.blink.sync['test'].start() self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].name, 'test') self.assertEqual(self.blink.sync['test'].name, 'test')
self.assertEqual(self.blink.sync['test'].sync_id, 1234) self.assertEqual(self.blink.sync['test'].sync_id, 1234)
self.assertEqual(self.blink.sync['test'].network_id, 5678) self.assertEqual(self.blink.sync['test'].network_id, 5678)
self.assertEqual(self.blink.sync['test'].serial, '12345678') self.assertEqual(self.blink.sync['test'].serial, '12345678')
self.assertEqual(self.blink.sync['test'].status, 'foobar') self.assertEqual(self.blink.sync['test'].status, 'foobar')
def test_unexpected_summary(self, mock_resp):
"""Test unexpected summary response."""
self.mock_start[0] = None
mock_resp.side_effect = self.mock_start
self.assertFalse(self.blink.sync['test'].start())
def test_summary_with_no_network_id(self, mock_resp):
"""Test handling of bad summary."""
self.mock_start[0]['syncmodule'] = None
mock_resp.side_effect = self.mock_start
self.assertFalse(self.blink.sync['test'].start())
def test_summary_with_only_network_id(self, mock_resp):
"""Test handling of sparse summary."""
self.mock_start[0]['syncmodule'] = {'network_id': 8675309}
mock_resp.side_effect = self.mock_start
self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].network_id, 8675309)
def test_unexpected_camera_info(self, mock_resp):
"""Test unexpected camera info response."""
self.blink.sync['test'].cameras['foo'] = None
self.mock_start[5] = None
mock_resp.side_effect = self.mock_start
self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].cameras, {'foo': None})
def test_missing_camera_info(self, mock_resp):
"""Test missing key from camera info response."""
self.blink.sync['test'].cameras['foo'] = None
self.mock_start[5] = {}
self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].cameras, {'foo': None})
+114
View File
@@ -0,0 +1,114 @@
"""Test various api functions."""
import unittest
from unittest import mock
import time
from blinkpy.helpers.util import Throttle, BlinkURLHandler, time_to_seconds
class TestUtil(unittest.TestCase):
"""Test the helpers/util module."""
def setUp(self):
"""Initialize the blink module."""
def tearDown(self):
"""Tear down blink module."""
def test_throttle(self):
"""Test the throttle decorator."""
calls = []
@Throttle(seconds=5)
def test_throttle():
calls.append(1)
now = int(time.time())
now_plus_four = now + 4
now_plus_six = now + 6
test_throttle()
self.assertEqual(1, len(calls))
# Call again, still shouldn't fire
test_throttle()
self.assertEqual(1, len(calls))
# Call with force
test_throttle(force=True)
self.assertEqual(2, len(calls))
# Call without throttle, shouldn't fire
test_throttle()
self.assertEqual(2, len(calls))
# Fake time as 4 seconds from now
with mock.patch('time.time', return_value=now_plus_four):
test_throttle()
self.assertEqual(2, len(calls))
# Fake time as 6 seconds from now
with mock.patch('time.time', return_value=now_plus_six):
test_throttle()
self.assertEqual(3, len(calls))
def test_throttle_per_instance(self):
"""Test that throttle is done once per instance of class."""
class Tester:
"""A tester class for throttling."""
def test(self):
"""Test the throttle."""
return True
tester = Tester()
throttled = Throttle(seconds=1)(tester.test)
self.assertEqual(throttled(), True)
self.assertEqual(throttled(), None)
def test_throttle_on_two_methods(self):
"""Test that throttle works for multiple methods."""
class Tester:
"""A tester class for throttling."""
@Throttle(seconds=3)
def test1(self):
"""Test function for throttle."""
return True
@Throttle(seconds=5)
def test2(self):
"""Test function for throttle."""
return True
tester = Tester()
now = time.time()
now_plus_4 = now + 4
now_plus_6 = now + 6
self.assertEqual(tester.test1(), True)
self.assertEqual(tester.test2(), True)
self.assertEqual(tester.test1(), None)
self.assertEqual(tester.test2(), None)
with mock.patch('time.time', return_value=now_plus_4):
self.assertEqual(tester.test1(), True)
self.assertEqual(tester.test2(), None)
with mock.patch('time.time', return_value=now_plus_6):
self.assertEqual(tester.test1(), None)
self.assertEqual(tester.test2(), True)
def test_legacy_subdomains(self):
"""Test that subdomain can be set to legacy mode."""
urls = BlinkURLHandler('test')
self.assertEqual(urls.subdomain, 'rest-test')
urls = BlinkURLHandler('test', legacy=True)
self.assertEqual(urls.subdomain, 'rest.test')
def test_time_to_seconds(self):
"""Test time to seconds conversion."""
correct_time = '1970-01-01T00:00:05+00:00'
wrong_time = '1/1/1970 00:00:03'
self.assertEqual(time_to_seconds(correct_time), 5)
self.assertFalse(time_to_seconds(wrong_time))
+15 -5
View File
@@ -8,11 +8,21 @@ setenv =
LANG=en_US.UTF-8 LANG=en_US.UTF-8
PYTHONPATH = {toxinidir} PYTHONPATH = {toxinidir}
commands = commands =
py.test --timeout=30 --duration=10 --cov=blinkpy --cov-report term-missing {posargs} pytest --timeout=9 --durations=10 --cov=blinkpy --cov-report term-missing {posargs}
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/requirements_test.txt -r{toxinidir}/requirements_test.txt
[testenv:cov]
setenv =
LANG=en_US.UTF-8
PYTHONPATH = {toxinidir}
commands =
pytest --timeout=9 --durations=10 --cov=blinkpy --cov-report=xml {posargs}
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements_test.txt
[testenv:lint] [testenv:lint]
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
@@ -20,9 +30,9 @@ deps =
basepython = python3 basepython = python3
ignore_errors = True ignore_errors = True
commands = commands =
pylint --rcfile={toxinidir}/pylintrc blinkpy tests pylint --rcfile={toxinidir}/pylintrc blinkpy tests app
flake8 blinkpy tests flake8 blinkpy tests app
pydocstyle blinkpy tests pydocstyle blinkpy tests app
rst-lint README.rst rst-lint README.rst
rst-lint CHANGES.rst rst-lint CHANGES.rst