Compare commits

..
247 Commits
Author SHA1 Message Date
Kevin FronczakandGitHub e3f9f4b0c1 Merge pull request #245 from fronzbot/add-device-id-param
Add device id param
2020-05-06 18:00:43 -04:00
Kevin Fronczak ec476bb9d6 quick codecov threshold fix...maybe 2020-05-06 11:19:45 -04:00
Kevin Fronczak 60cbcb7717 Update login methods to ensure platform is refreshed 2020-05-06 11:12:56 -04:00
Kevin Fronczak 4b11a8b16a Add ability to have unique device name per application 2020-05-06 10:59:01 -04:00
Kevin FronczakandGitHub dfb90b8c86 Dev version bump 2020-05-06 10:43:09 -04:00
Kevin FronczakandGitHub 2f0cec2724 Merge pull request #243 from fronzbot/dependabot/pip/pylint-2.5.2
Bump pylint from 2.5.1 to 2.5.2
2020-05-06 10:42:29 -04:00
Kevin FronczakandGitHub 887a1d0a2b Merge pull request #244 from fronzbot/fix-typos
Fix typos
2020-05-06 10:42:11 -04:00
Kevin FronczakandGitHub 1d65d558e8 Fix typo 2020-05-06 10:39:34 -04:00
Kevin FronczakandGitHub d49a1fe36c Fix typo, remove Python Version info 2020-05-06 10:37:25 -04:00
dependabot-preview[bot]andGitHub 33f0e16e7a Bump pylint from 2.5.1 to 2.5.2
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.5.1 to 2.5.2.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/master/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/pylint-2.5.1...pylint-2.5.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-06 08:37:35 +00:00
Kevin FronczakandGitHub 86aae07367 Merge pull request #242 from fronzbot/add-2FA
Add 2FA Support
2020-05-05 22:51:45 -04:00
Kevin Fronczak 1e86e7eb79 Add unit tests for new methods 2020-05-05 22:45:48 -04:00
Kevin Fronczak 0c7737cd7f Update README 2020-05-05 15:38:45 -04:00
Kevin Fronczak 0c79620baa Updated login method to allow for email verification and 2FA in future 2020-05-05 15:28:38 -04:00
Kevin FronczakandGitHub 9bc20cc82e Merge pull request #241 from fronzbot/refactor-login-logic
Refactor login logic in preperation for 2FA
2020-05-05 13:56:39 -04:00
Kevin Fronczak c51551afc5 Add skeleton for 2FA 2020-05-05 13:56:04 -04:00
Kevin Fronczak f80a93f8e1 Refactor login logic in preperation for 2FA 2020-05-05 12:46:48 -04:00
Kevin FronczakandGitHub 853c596167 Merge pull request #240 from fronzbot/dependabot/pip/pylint-2.5.1
Bump pylint from 2.5.0 to 2.5.1
2020-05-05 08:10:58 -04:00
dependabot-preview[bot]andGitHub 819e383b05 Bump pylint from 2.5.0 to 2.5.1
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.5.0 to 2.5.1.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/master/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/pylint-2.5.0...pylint-2.5.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-05 08:29:12 +00:00
Kevin FronczakandGitHub 724734847a Merge pull request #239 from fronzbot/codecov-threshold
Fix codecov threshold
2020-05-04 14:44:53 -04:00
Kevin Fronczak cdd01c5186 Fix codecov threshold 2020-05-04 14:38:47 -04:00
Kevin FronczakandGitHub 5a5c34b6a1 Merge pull request #238 from fronzbot/add-black
Add black
2020-05-04 14:37:50 -04:00
Kevin Fronczak 28271efbe6 Remove hound. 2020-05-04 14:30:48 -04:00
Kevin Fronczak 1dbb8d4b21 Add black badge 2020-05-04 14:28:14 -04:00
Kevin Fronczak e1bbbb5c7e Add black formatting style 2020-05-04 14:23:02 -04:00
Kevin FronczakandGitHub addc32e3e6 Merge pull request #237 from fronzbot/deprecate-py35
Remove Python 3.5.3 support
2020-05-04 13:41:11 -04:00
Kevin Fronczak b201c95e8d Remove Python 3.5.3 support 2020-05-04 13:37:32 -04:00
Kevin FronczakandGitHub 19b2541c81 Merge pull request #236 from fronzbot/remove-flake8-filewq
Remove .flake8 file
2020-05-04 13:31:57 -04:00
Kevin Fronczak 6a2650dda0 Remove .flake8 file 2020-05-04 13:28:37 -04:00
Kevin FronczakandGitHub c122c4ecfb Merge pull request #235 from fronzbot/dependabot/pip/pylint-2.5.0
Bump pylint from 2.4.4 to 2.5.0
2020-05-04 13:21:22 -04:00
Kevin Fronczak 43ed6f1600 Fix lint errors 2020-05-04 13:14:52 -04:00
Kevin FronczakandGitHub fae1e1a554 Merge pull request #230 from fronzbot/dependabot/pip/python-dateutil-approx-eq-2.8.1
Update python-dateutil requirement from ~=2.8.0 to ~=2.8.1
2020-05-04 12:39:11 -04:00
Kevin Fronczak 4587268ecb Bump dateutil to 2.8.1 2020-05-04 12:32:40 -04:00
dependabot-preview[bot]andGitHub e0ff5c484a Update python-dateutil requirement from ~=2.8.0 to ~=2.8.1
Updates the requirements on [python-dateutil](https://github.com/dateutil/dateutil) to permit the latest version.
- [Release notes](https://github.com/dateutil/dateutil/releases)
- [Changelog](https://github.com/dateutil/dateutil/blob/master/NEWS)
- [Commits](https://github.com/dateutil/dateutil/compare/2.8.0...2.8.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-04 16:28:09 +00:00
Kevin FronczakandGitHub bc4956c64b Merge pull request #234 from fronzbot/dependabot/pip/python-slugify-approx-eq-4.0.0
Update python-slugify requirement from ~=3.0.2 to ~=4.0.0
2020-05-04 12:26:45 -04:00
Kevin Fronczak cc5fbcd3a7 Bump slugify to 4.0.0 2020-05-04 12:19:30 -04:00
dependabot-preview[bot]andGitHub db23acd46c Update python-slugify requirement from ~=3.0.2 to ~=4.0.0
Updates the requirements on [python-slugify](https://github.com/un33k/python-slugify) to permit the latest version.
- [Release notes](https://github.com/un33k/python-slugify/releases)
- [Changelog](https://github.com/un33k/python-slugify/blob/master/CHANGELOG.md)
- [Commits](https://github.com/un33k/python-slugify/compare/3.0.2...4.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-04 16:17:35 +00:00
Kevin FronczakandGitHub 973374f6a1 Merge pull request #227 from fronzbot/dependabot/pip/testtools-2.4.0
Bump testtools from 2.3.0 to 2.4.0
2020-05-04 12:16:25 -04:00
dependabot-preview[bot]andGitHub 13e2262c2f Bump pylint from 2.4.4 to 2.5.0
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.4.4 to 2.5.0.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/master/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/pylint-2.4.4...pylint-2.5.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-04 16:13:02 +00:00
Kevin FronczakandGitHub 69775188a6 Merge pull request #228 from fronzbot/dependabot/pip/coverage-5.1
Bump coverage from 4.5.4 to 5.1
2020-05-04 12:12:10 -04:00
Kevin FronczakandGitHub 0554e4e159 Merge pull request #229 from fronzbot/dependabot/pip/pytest-5.4.1
Bump pytest from 5.3.5 to 5.4.1
2020-05-04 12:11:44 -04:00
dependabot-preview[bot]andGitHub f576c97c1d Bump coverage from 4.5.4 to 5.1
Bumps [coverage](https://github.com/nedbat/coveragepy) from 4.5.4 to 5.1.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/coverage-4.5.4...coverage-5.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-04 15:59:53 +00:00
dependabot-preview[bot]andGitHub e6c842072c Bump pytest from 5.3.5 to 5.4.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.3.5 to 5.4.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/5.3.5...5.4.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-04 15:59:16 +00:00
dependabot-preview[bot]andGitHub bf87f3676d Bump testtools from 2.3.0 to 2.4.0
Bumps [testtools](https://github.com/testing-cabal/testtools) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/testing-cabal/testtools/releases)
- [Changelog](https://github.com/testing-cabal/testtools/blob/master/NEWS)
- [Commits](https://github.com/testing-cabal/testtools/compare/2.3.0...2.4.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-04 15:52:19 +00:00
Kevin FronczakandGitHub a87581638c Merge pull request #231 from fronzbot/dependabot/pip/requests-2.23.0
Bump requests from 2.22.0 to 2.23.0
2020-05-04 11:51:09 -04:00
Kevin Fronczak 42ed18fcd5 Bump requests to 2.23.0 2020-05-04 11:45:54 -04:00
Kevin FronczakandGitHub a66c926475 Merge pull request #232 from fronzbot/stale-bot
Create stale.yml
2020-05-04 11:40:53 -04:00
Kevin FronczakandGitHub 6a481aaa74 Update stale.yml 2020-05-04 11:33:22 -04:00
dependabot-preview[bot]andGitHub 72d97f4944 Bump requests from 2.22.0 to 2.23.0
Bumps [requests](https://github.com/psf/requests) from 2.22.0 to 2.23.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/master/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.22.0...v2.23.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-05-04 15:30:44 +00:00
Kevin FronczakandGitHub 1694187b39 Merge pull request #233 from fronzbot/pip-downgrade-workflows
Fix setup.py use of internal pip structure
2020-05-04 11:29:30 -04:00
Kevin Fronczak a531a8e8a3 Update setup.py to not rely on internal pip functions 2020-05-04 11:05:46 -04:00
Kevin FronczakandGitHub 44d5bb65ae Exempt stuff added to projects and milestones 2020-05-04 10:21:06 -04:00
Kevin FronczakandGitHub c64f5cfb23 Downgrade pip again 2020-05-04 10:17:13 -04:00
Kevin FronczakandGitHub 19a29bc3a1 Create stale.yml 2020-05-04 10:12:41 -04:00
Kevin FronczakandGitHub 6159987928 Downgrade pip 2020-05-04 09:57:23 -04:00
Kevin FronczakandGitHub 8a8c4ea20b Change to SemVer for dev versions (now rc) 2020-05-04 09:31:41 -04:00
Kevin FronczakandGitHub e43d2f9a88 Dev version bump 2020-04-22 10:37:43 -04:00
Kevin FronczakandGitHub 0c3359abcf Changes for release 0.14.3 2020-04-22 10:20:42 -04:00
Kevin FronczakandGitHub a76069174d Version bump to 0.14.3 2020-04-22 10:17:21 -04:00
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
40 changed files with 2269 additions and 910 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
[run]
omit =
helpers/*
parallel = true
omit =
tests/*
setup.py
setup.py
+30
View File
@@ -0,0 +1,30 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- Priority
- bug
- Help Wanted
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs within seven days. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
This issue is now being closed due to inactivity.
+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: 3
matrix:
platform:
- ubuntu-latest
python-version: [3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements_test.txt
pip install tox
- name: Test
run: |
tox -r
+2 -1
View File
@@ -3,7 +3,8 @@
.tox/*
__pycache__/*
htmlcov/*
.coverage
.coverage.*
coverage.xml
*.pyc
*.egg*/*
dist/*
-7
View File
@@ -1,7 +0,0 @@
python:
enabled: true
ignore_file: setup.py
ignored_directories:
- docs
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
Unofficial documentation for the Client API of the Blink Wire-Free HD Home Monitoring and Alert System.
Unofficial documentation for the Client API of the Blink Wire-Free HD Home Monitoring &amp; Alert System.
Copied from https://github.com/MattTW/BlinkMonitorProtocol
@@ -7,9 +7,6 @@ I am not affiliated with the company in any way - this documentation is strictly
The Client API is a straightforward REST API using JSON and HTTPS.
## This Document
The purpose here is to describe what is going on behind the scenes, and what commands we know are available to communicate with Blink servers. This is NOT a description of the blinkpy module, but a description of the commands blinkpy relies on to effectively communicate.
## Login
Client login to the Blink Servers.
@@ -19,20 +16,25 @@ Client login to the Blink Servers.
> "password" : "*your blink password*",
> "client_specifier" : "iPhone 9.2 | 2.2 | 222",
> "email" : "*your blink login/email*"
>}' --compressed https://prod.immedia-semi.com/login
>}' --compressed https://rest.prod.immedia-semi.com/login
**Response:**
>{"authtoken":{"authtoken":"*an auth token*","message":"auth"}}
>{"authtoken":{"authtoken":"*an auth token*","message":"auth"},"networks":{"*network id*":{"name":"*name*","onboarded":true}},"region":{"*regioncode for endpoint*":"*region name"}}
**Notes:**
The authtoken value is passed in a header in future calls.
The region code for endpoint is required to form the URL of the REST endpoint for future calls.
Depending on the region you are registered you will need to change the REST endpoints below:
- from `https://rest.prod.immedia-semi.com`
- to `https://rest.prde.immedia-semi.com` if e.g. your device is registered in Germany
Please note that at this moment it seems that all regions are not implemented equally: not all endpoints are available in all regions
##Networks
## Networks
Obtain information about the Blink networks defined for the logged in user.
**Request:**
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/networks
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/networks
**Response:**
JSON response containing information including Network ID and Account ID.
@@ -46,7 +48,7 @@ Network ID is needed to issue arm/disarm calls
Obtain information about the Blink Sync Modules on the given network.
**Request:**
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id_from_networks_call*/syncmodules
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id_from_networks_call*/syncmodules
**Response:**
JSON response containing information about the known state of the Sync module, most notably if it is online
@@ -56,11 +58,11 @@ Probably not strictly needed but checking result can verify that the sync module
## Arm
Arm the given network (start recording/reporting motion events)
**Request:**
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id_from_networks_call*/arm
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id_from_networks_call*/arm
**Response:**
JSON response containing information about the arm command request, including the command/request ID
@@ -73,7 +75,7 @@ When this call returns, it does not mean the arm request is complete, the clien
Disarm the given network (stop recording/reporting motion events)
**Request:**
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id_from_networks_call*/disarm
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id_from_networks_call*/disarm
**Response:**
JSON response containing information about the disarm command request, including the command/request ID
@@ -87,7 +89,7 @@ When this call returns, it does not mean the disarm request is complete, the cl
Get status info on the given command
**Request:**
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/command/*command_id*
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/command/*command_id*
**Response:**
JSON response containing state information of the given command, most notably whether it has completed and was successful.
@@ -103,7 +105,7 @@ lv_relay, arm, disarm, thumbnail, clip
Return information displayed on the home screen of the mobile client
**Request:**
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/homescreen
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/homescreen
**Response:**
JSON response containing information that the mobile client displays on the home page, including: status, armed state, links to thumbnails for each camera, etc.
@@ -111,12 +113,12 @@ JSON response containing information that the mobile client displays on the home
**Notes:**
Not necessary to as part of issuing arm/disarm commands, but contains good summary info.
##Events, thumbnails & video captures
## Events, thumbnails & video captures
**Request**
Get events for a given network (sync module) -- Need network ID from home
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/events/network/*network__id*
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/events/network/*network__id*
**Response**
A json list of evets incluing URL's. Replace the "mp4" with "jpg" extension to get the thumbnail of each clip
@@ -144,7 +146,7 @@ Note that you replace the 'mp4' with a 'jpg' to get the thumbnail
**Request**
Captures a new thumbnail for a camera
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/thumbnail
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/thumbnail
**Response**
Command information.
@@ -152,17 +154,17 @@ Command information.
**Request**
Captures a new video for a camera
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/clip
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/clip
**Response**
Command information.
##Video Information
## Video Information
**Request**
Get the total number of videos in the system
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/videos/count
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/videos/count
**Response**
JSON response containing the total video count.
@@ -170,7 +172,7 @@ JSON response containing the total video count.
**Request**
Gets a paginated set of video information
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/videos/page/0
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/videos/page/0
**Response**
JSON response containing a set of video information, including: camera name, creation time, thumbnail URI, size, length
@@ -178,7 +180,7 @@ JSON response containing a set of video information, including: camera name, cre
**Request**
Gets information for a specific video by ID
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/video/*video_id*
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/video/*video_id*
**Response**
JSON response containing video information
@@ -186,7 +188,7 @@ JSON response containing video information
**Request**
Gets a list of unwatched videos
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v2/videos/unwatched
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v2/videos/unwatched
**Response**
JSON response containing unwatched video information
@@ -194,7 +196,7 @@ JSON response containing unwatched video information
**Request**
Deletes a video
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/api/v2/video/*video_id*/delete
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/api/v2/video/*video_id*/delete
**Response**
Unknown - not tested
@@ -202,7 +204,7 @@ Unknown - not tested
**Request**
Deletes all videos
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://prod.immedia-semi.com/api/v2/videos/deleteall
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --data-binary --compressed https://rest.prod.immedia-semi.com/api/v2/videos/deleteall
**Response**
Unknown - not tested
@@ -212,7 +214,7 @@ Unknown - not tested
**Request**
Gets a list of cameras
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/cameras
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/cameras
**Response**
JSON response containing camera information
@@ -220,7 +222,7 @@ JSON response containing camera information
**Request**
Gets information for one camera
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*
**Response**
JSON response containing camera information
@@ -228,7 +230,7 @@ JSON response containing camera information
**Request**
Gets camera sensor information
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/signals
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/signals
**Response**
JSON response containing camera sensor information, such as wifi strength, temperature, and battery level
@@ -236,7 +238,7 @@ JSON response containing camera sensor information, such as wifi strength, tempe
**Request**
Enables motion detection for one camera
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/enable
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/enable
**Response**
JSON response containing camera information
@@ -244,7 +246,7 @@ JSON response containing camera information
**Request**
Disables motion detection for one camera
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/disable
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: $auth_token" --data-binary --compressed https://rest.prod.immedia-semi.com/network/*network_id*/camera/*camera_id*/disable
**Response**
JSON response containing camera information
@@ -257,7 +259,7 @@ JSON response containing camera information
**Request**
Gets information about devices that have connected to the blink service
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/account/clients
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/account/clients
**Response**
JSON response containing client information, including: type, name, connection time, user ID
@@ -265,7 +267,7 @@ JSON response containing client information, including: type, name, connection t
**Request**
Gets information about supported regions
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/regions
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/regions
**Response**
JSON response containing region information
@@ -273,7 +275,7 @@ JSON response containing region information
**Request**
Gets information about system health
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/health
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/health
**Response**
"all ports tested are open"
@@ -281,7 +283,7 @@ Gets information about system health
**Request**
Gets information about programs
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://prod.immedia-semi.com/api/v1/networks/*network_id*/programs
>curl -H "Host: prod.immedia-semi.com" -H "TOKEN_AUTH: *authtoken from login*" --compressed https://rest.prod.immedia-semi.com/api/v1/networks/*network_id*/programs
**Response**
Unknown.
+99 -2
View File
@@ -3,6 +3,103 @@ Changelog
A list of changes between each release
0.14.3 (2020-04-22)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Add time check on recorded videos before determining motion
- Fix motion detection variable suck to ``True``
- Add ability to load credentials from a json file
- Only allow ``motion_detected`` variable to trigger if system was armed
- Log response message from server if not attempting a re-authorization
0.14.2 (2019-10-12)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Update dependencies
- Dockerize `(@3ch01c <https://github.com/fronzbot/blinkpy/pull/198>__)`
0.14.1 (2019-06-20)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Fix timeout problems blocking blinkpy startup
- Updated login urls using ``rest-region`` subdomain
- Removed deprecated thumbanil recovery from homescreen
0.14.0 (2019-05-23)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**Breaking Changes:**
- ``BlinkCamera.battery`` no longer reports a percentage, instead it returns a string representing the state of the battery.
- Previous logic for calculating percentage was incorrect
- raw battery voltage can be accessed via ``BlinkCamera.battery_voltage``
**Bug Fixes:**
- Updated video endpoint (fixes broken motion detection)
- Removed throttling from critical api methods which prevented proper operation of multi-sync unit setups
- Slugify downloaded video names to allow for OS interoperability
- Added one minute offset (``Blink.motion_interval``) when checking for recent motion to allow time for events to propagate to server prior to refresh call.
**Everything else:**
- Changed all urls to use ``rest-region`` rather than ``rest.region``. Ability to revert to old method is enabled by instantiating ``Blink()`` with the ``legacy_subdomain`` variable set to ``True``.
- Added debug mode to ``blinkpy.download_videos`` routine to simply print the videos prepped for download, rather than actually saving them.
- Use UTC for time conversions, rather than local timezone
0.13.1 (2019-03-01)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Remove throttle decorator from network status request
0.13.0 (2019-03-01)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**Breaking change:**
Wifi status reported in dBm again, instead of bars (which is great). Also, the old ``get_camera_info`` method has changed and requires a ``camera_id`` parameter.
- Adds throttle decorator
- Decorate following functions with 4s throttle (call method with ``force=True`` to override):
- request_network_status
- request_syncmodule
- request_system_arm
- request_system_disarm
- request_sync_events
- request_new_image
- request_new_video
- request_video_count
- request_cameras
- request_camera_info
- request_camera_sensors
- request_motion_detection_enable
- request_motion_detection_disable
- Use the updated homescreen api endpoint to retrieve camera information. The old method to retrieve all cameras at once seems to not exist, and this was the only solution I could figure out and confirm to work.
- Adds throttle decorator to refresh function to prevent too many frequent calls with ``force_cache`` flag set to ``True``. This additional throttle can be overridden with the ``force=True`` argument passed to the refresh function.
- Add ability to cycle through login api endpoints to anticipate future endpoint deprecation
0.12.1 (2019-01-31)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Remove logging improvements since they were incompatible with home-assistant logging
0.12.0 (2019-01-31)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Fix video api endpoint, re-enables motion detection
- Add improved logging capability
- Add download video method
- Prevent blinkpy from failing at setup due to api error
0.11.2 (2019-01-23)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Hotfix to prevent platform from stalling due to API change
- Motion detection and video recovery broken until new API endpoint discovered
0.11.1 (2019-01-02)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Fixed incorrect backup login url
- Added calibrated temperature property for cameras
0.11.0 (2018-11-23)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Added support for multiple sync modules
0.10.3 (2018-11-18)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Use networks endpoint rather than homecreen to retrieve arm/disarm status (`@md-reddevil <https://github.com/fronzbot/blinkpy/pull/119>`__)
@@ -14,7 +111,7 @@ A list of changes between each release
0.10.2 (2018-10-30)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Set minimum required version of the requests library to 2.20.0 due to vulnerability in earlier releases.
- When multiple networks detected, changed log level to 'warning' from 'error'
- When multiple networks detected, changed log level to ``warning`` from ``error``
0.10.1 (2018-10-18)
@@ -42,7 +139,7 @@ A list of changes between each release
- Use session for http requests
**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)
+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 []
+50 -11
View File
@@ -1,6 +1,10 @@
blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version| |Python Version|
================================================================================
A Python library for the Blink Camera system
blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version| |Codestyle|
=============================================================================================
A Python library for the Blink Camera system (Python 3.6+)
Like the library? Consider buying me a cup of coffee!
|Donate|
Disclaimer:
~~~~~~~~~~~~~~~
@@ -49,7 +53,29 @@ The simplest way to use this package from a terminal is to call ``Blink.start()`
blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD', refresh_rate=30)
blink.start()
If you would like to log in without setting up the cameras or system, you can simply call the ``Blink.login()`` function which will prompt for a username and password and then authenticate with the server. This is useful if you want to avoid use of the ``start()`` function which simply acts as a wrapper for more targeted API methods.
At startup, you may be prompted for a verification key. Just enter this in the command-line prompt. If you just receive a verification email asking to validate access for your device, enter nothing at this prompt. To avoid any command-line interaction, call the ``Blink`` class with the ``no_prompt=True`` flag. Instead, once you receive the verification email, call the following functions:
.. code:: python
blink.login_handler.send_auth_key(blink, VERIFICATION_KEY)
blink.setup_post_verify()
In addition, you can also save your credentials in a json file and initialize Blink with the credential file as follows:
.. code:: python
from blinkpy import blinkpy
blink = blinkpy.Blink(cred_file="path/to/credentials.json")
blink.start()
The credential file must be json formatted with a ``username`` and ``password`` key like follows:
.. code:: json
{
"username": "YOUR USER NAME",
"password": "YOUR PASSWORD"
}
Cameras are instantiated as individual ``BlinkCamera`` classes within a ``BlinkSyncModule`` instance. All of your sync modules are stored within the ``Blink.sync`` dictionary and can be accessed using the name of the sync module as the key (this is the name of your sync module in the Blink App).
@@ -84,14 +110,27 @@ The ``blinkpy`` api also allows for saving images and videos to a file and snapp
blink.refresh() # Get new information from server
camera.image_to_file('/local/path/for/image.jpg')
camera.video_to_file('/local/path/for/video.mp4')
You can also use this library to download all videos from the server. In order to do this, you must specify a ``path``. You may also specifiy a how far back in time to go to retrieve videos via the ``since=`` variable (a simple string such as ``"2017/09/21"`` is sufficient), as well as how many pages to traverse via the ``page=`` variable. Note that by default, the library will search the first ten pages which is sufficient in most use cases. Additionally, you can specidy one or more cameras via the ``camera=`` property. This can be a single string indicating the name of the camera, or a list of camera names. By default, it is set to the string ``'all'`` to grab videos from all cameras.
.. |Build Status| image:: https://travis-ci.org/fronzbot/blinkpy.svg?branch=dev
:target: https://travis-ci.org/fronzbot/blinkpy
.. |Coverage Status| image:: https://coveralls.io/repos/github/fronzbot/blinkpy/badge.svg?branch=dev
:target: https://coveralls.io/github/fronzbot/blinkpy?branch=dev
Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34am to the ``/home/blink`` directory:
.. code:: python
blink = blinkpy.Blink(username="YOUR USER NAME", password="YOUR PASSWORD")
blink.start()
blink.download_videos('/home/blink', since='2018/07/04 09:34')
.. |Build Status| image:: https://github.com/fronzbot/blinkpy/workflows/build/badge.svg
:target: https://github.com/fronzbot/blinkpy/actions?query=workflow%3Abuild
.. |Coverage Status| image:: https://codecov.io/gh/fronzbot/blinkpy/branch/dev/graph/badge.svg
:target: https://codecov.io/gh/fronzbot/blinkpy
.. |PyPi Version| image:: https://img.shields.io/pypi/v/blinkpy.svg
:target: https://pypi.python.org/pypi/blinkpy
.. |Docs| image:: https://readthedocs.org/projects/blinkpy/badge/?version=latest
:target: http://blinkpy.readthedocs.io/en/latest/?badge=latest
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/blinkpy.svg
:target: https://img.shields.io/pypi/pyversions/blinkpy.svg
:target: http://blinkpy.readthedocs.io/en/latest/?badge=latest
.. |Donate| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UR6Z2B8GXYUCC
.. |Codestyle| image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
+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
+208 -58
View File
@@ -3,25 +3,77 @@
import logging
from json import dumps
import blinkpy.helpers.errors as ERROR
from blinkpy.helpers.util import http_req, BlinkException
from blinkpy.helpers.util import http_req, get_time, BlinkException, Throttle
from blinkpy.helpers.constants import DEFAULT_URL
_LOGGER = logging.getLogger(__name__)
MIN_THROTTLE_TIME = 2
def request_login(blink, url, username, password):
"""Login request."""
headers = {
'Host': DEFAULT_URL,
'Content-Type': 'application/json'
}
data = dumps({
'email': username,
'password': password,
'client_specifier': 'iPhone 9.2 | 2.2 | 222'
})
return http_req(blink, url=url, headers=headers, data=data,
json_resp=False, reqtype='post')
def request_login(
blink,
url,
username,
password,
notification_key,
uid,
is_retry=False,
device_id="Blinkpy",
):
"""
Login request.
:param blink: Blink instance.
:param url: Login url.
:param username: Blink username.
:param password: Blink password.
:param notification_key: Randomly genereated key.
:param uid: Randomly generated unique id key.
:param is_retry: Is this part of a re-authorization attempt?
:param device_id: Name of application to send at login.
"""
headers = {"Host": DEFAULT_URL, "Content-Type": "application/json"}
data = dumps(
{
"email": username,
"password": password,
"notification_key": notification_key,
"unique_id": uid,
"app_version": "6.0.7 (520300) #afb0be72a",
"client_name": "Computer",
"client_type": "android",
"device_identifier": device_id,
"device_name": "Blinkpy",
"os_version": "5.1.1",
"reauth": "true",
}
)
return http_req(
blink,
url=url,
headers=headers,
data=data,
json_resp=False,
reqtype="post",
is_retry=is_retry,
)
def request_verify(blink, verify_key):
"""Send verification key to blink servers."""
url = "{}/api/v4/account/{}/client/{}/pin/verify".format(
blink.urls.base_url, blink.account_id, blink.client_id
)
data = dumps({"pin": verify_key})
return http_req(
blink,
url=url,
headers=blink.auth_header,
data=data,
json_resp=False,
reqtype="post",
)
def request_networks(blink):
@@ -31,137 +83,235 @@ def request_networks(blink):
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)
return http_get(blink, url)
def request_syncmodule(blink, network):
"""Request sync module info."""
"""
Request sync module info.
:param blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/network/{}/syncmodules".format(blink.urls.base_url, network)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_system_arm(blink, network):
"""Arm system."""
"""
Arm system.
:param blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/network/{}/arm".format(blink.urls.base_url, network)
return http_post(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_system_disarm(blink, network):
"""Disarm system."""
"""
Disarm system.
:param blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/network/{}/disarm".format(blink.urls.base_url, network)
return http_post(blink, url)
def request_command_status(blink, network, command_id):
"""Request command status."""
url = "{}/network/{}/command/{}".format(blink.urls.base_url,
network,
command_id)
"""
Request command status.
:param blink: Blink instance.
:param network: Sync module network id.
:param command_id: Command id to check.
"""
url = "{}/network/{}/command/{}".format(blink.urls.base_url, network, command_id)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_homescreen(blink):
"""Request homescreen info."""
url = "{}/homescreen".format(blink.urls.base_url)
url = "{}/api/v3/accounts/{}/homescreen".format(
blink.urls.base_url, blink.account_id
)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_sync_events(blink, network):
"""Request events from sync module."""
"""
Request events from sync module.
:param blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/events/network/{}".format(blink.urls.base_url, network)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_new_image(blink, network, camera_id):
"""Request to capture new thumbnail for camera."""
url = "{}/network/{}/camera/{}/thumbnail".format(blink.urls.base_url,
network,
camera_id)
"""
Request to capture new thumbnail for camera.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request new image from.
"""
url = "{}/network/{}/camera/{}/thumbnail".format(
blink.urls.base_url, network, camera_id
)
return http_post(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_new_video(blink, network, camera_id):
"""Request to capture new video clip."""
url = "{}/network/{}/camera/{}/clip".format(blink.urls.base_url,
network,
camera_id)
"""
Request to capture new video clip.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request new video from.
"""
url = "{}/network/{}/camera/{}/clip".format(blink.urls.base_url, network, camera_id)
return http_post(blink, url)
def request_video_count(blink, headers):
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_video_count(blink):
"""Request total video count."""
url = "{}/api/v2/videos/count".format(blink.urls.base_url)
return http_get(blink, url)
def request_videos(blink, page=0):
"""Perform a request for videos."""
url = "{}/api/v2/videos/page/{}".format(blink.urls.base_url, page)
def request_videos(blink, time=None, page=0):
"""
Perform a request for videos.
:param blink: Blink instance.
:param time: Get videos since this time. In epoch seconds.
:param page: Page number to get videos from.
"""
timestamp = get_time(time)
url = "{}/api/v1/accounts/{}/media/changed?since={}&page={}".format(
blink.urls.base_url, blink.account_id, timestamp, page
)
return http_get(blink, url)
def request_cameras(blink, network):
"""Request all camera information."""
"""
Request all camera information.
:param Blink: Blink instance.
:param network: Sync module network id.
"""
url = "{}/network/{}/cameras".format(blink.urls.base_url, network)
return http_get(blink, url)
def request_camera_info(blink, network, camera_id):
"""Request camera info for one camera."""
url = "{}/network/{}/camera/{}".format(blink.urls.base_url,
network,
camera_id)
"""
Request camera info for one camera.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request info from.
"""
url = "{}/network/{}/camera/{}/config".format(
blink.urls.base_url, network, camera_id
)
return http_get(blink, url)
def request_camera_sensors(blink, network, camera_id):
"""Request camera sensor info for one camera."""
url = "{}/network/{}/camera/{}/signals".format(blink.urls.base_url,
network,
camera_id)
"""
Request camera sensor info for one camera.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request sesnor info from.
"""
url = "{}/network/{}/camera/{}/signals".format(
blink.urls.base_url, network, camera_id
)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_motion_detection_enable(blink, network, camera_id):
"""Enable motion detection for a camera."""
url = "{}/network/{}/camera/{}/enable".format(blink.urls.base_url,
network,
camera_id)
"""
Enable motion detection for a camera.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to enable.
"""
url = "{}/network/{}/camera/{}/enable".format(
blink.urls.base_url, network, camera_id
)
return http_post(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_motion_detection_disable(blink, network, camera_id):
"""Disable motion detection for a camera."""
url = "{}/network/{}/camera/{}/disable".format(blink.urls.base_url,
network,
camera_id)
"""Disable motion detection for a camera.
:param blink: Blink instance.
:param network: Sync module network id.
:param camera_id: Camera ID of camera to disable.
"""
url = "{}/network/{}/camera/{}/disable".format(
blink.urls.base_url, network, camera_id
)
return http_post(blink, url)
def http_get(blink, url, stream=False, json=True):
def http_get(blink, url, stream=False, json=True, is_retry=False):
"""
Perform an http get request.
:param url: URL to perform get request.
:param stream: Stream response? True/FALSE
:param json: Return json response? TRUE/False
:param is_retry: Is this part of a re-auth attempt?
"""
if blink.auth_header is None:
raise BlinkException(ERROR.AUTH_TOKEN)
_LOGGER.debug("Making GET request to %s", url)
return http_req(blink, url=url, headers=blink.auth_header,
reqtype='get', stream=stream, json_resp=json)
return http_req(
blink,
url=url,
headers=blink.auth_header,
reqtype="get",
stream=stream,
json_resp=json,
is_retry=is_retry,
)
def http_post(blink, url):
def http_post(blink, url, is_retry=False):
"""
Perform an http post request.
:param url: URL to perfom post request.
:param is_retry: Is this part of a re-auth attempt?
"""
if blink.auth_header is None:
raise BlinkException(ERROR.AUTH_TOKEN)
_LOGGER.debug("Making POST request to %s", url)
return http_req(blink, url=url, headers=blink.auth_header, reqtype='post')
return http_req(
blink, url=url, headers=blink.auth_header, reqtype="post", is_retry=is_retry
)
+242 -83
View File
@@ -1,53 +1,103 @@
# -*- coding: utf-8 -*-
"""
blinkpy by Kevin Fronczak - A Blink camera Python library.
blinkpy is an unofficial api for the Blink security camera system.
repo url: https://github.com/fronzbot/blinkpy
https://github.com/fronzbot/blinkpy
Original protocol hacking by MattTW :
https://github.com/MattTW/BlinkMonitorProtocol
Published under the MIT license - See LICENSE file for more details.
"Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark
owned by Immedia Inc., see www.blinkforhome.com for more information.
I am in no way affiliated with Blink, nor Immedia Inc.
blinkpy is in no way affiliated with Blink, nor Immedia Inc.
"""
import os.path
import time
import getpass
import logging
from shutil import copyfileobj
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.sync_module import BlinkSyncModule
from blinkpy.helpers.util import (
create_session, merge_dicts, BlinkURLHandler,
BlinkAuthenticationException)
create_session,
merge_dicts,
get_time,
BlinkURLHandler,
Throttle,
)
from blinkpy.helpers.constants import (
BLINK_URL, LOGIN_URL, LOGIN_BACKUP_URL)
REFRESH_RATE = 30
_LOGGER = logging.getLogger('blinkpy')
BLINK_URL,
DEFAULT_MOTION_INTERVAL,
DEFAULT_REFRESH,
MIN_THROTTLE_TIME,
LOGIN_URLS,
)
from blinkpy.helpers.constants import __version__
from blinkpy.login_handler import LoginHandler
class Blink():
_LOGGER = logging.getLogger(__name__)
class Blink:
"""Class to initialize communication."""
def __init__(self, username=None, password=None,
refresh_rate=REFRESH_RATE):
def __init__(
self,
username=None,
password=None,
cred_file=None,
refresh_rate=DEFAULT_REFRESH,
motion_interval=DEFAULT_MOTION_INTERVAL,
legacy_subdomain=False,
no_prompt=False,
persist_key=None,
device_id="Blinkpy",
):
"""
Initialize Blink system.
:param username: Blink username (usually email address)
:param password: Blink password
:param cred_file: JSON formatted file to store credentials.
If username and password are given, file
is ignored. Otherwise, username and password
are loaded from file.
:param refresh_rate: Refresh rate of blink information.
Defaults to 15 (seconds)
:param motion_interval: How far back to register motion in minutes.
Defaults to last refresh time.
Useful for preventing motion_detected property
from de-asserting too quickly.
:param legacy_subdomain: Set to TRUE to use old 'rest.region'
endpoints (only use if you are having
api issues).
:param no_prompt: Set to TRUE if using an implementation that needs to
suppress command-line output.
:param persist_key: Location of persistant identifier.
:param device_id: Identifier for the application. Default is 'Blinkpy'.
This is used when logging in and should be changed to
fit the implementation (ie. "Home Assistant" in a
Home Assistant integration).
"""
self._username = username
self._password = password
self.login_handler = LoginHandler(
username=username,
password=password,
cred_file=cred_file,
persist_key=persist_key,
device_id=device_id,
)
self._token = None
self._auth_header = None
self._host = None
self.account_id = None
self.client_id = None
self.network_ids = []
self.urls = None
self.sync = CaseInsensitiveDict({})
@@ -55,10 +105,19 @@ class Blink():
self.region_id = None
self.last_refresh = None
self.refresh_rate = refresh_rate
self.session = None
self.session = create_session()
self.networks = []
self.cameras = CaseInsensitiveDict({})
self._login_url = LOGIN_URL
self.video_list = CaseInsensitiveDict({})
self.login_url = LOGIN_URLS[0]
self.login_urls = []
self.motion_interval = motion_interval
self.version = __version__
self.legacy = legacy_subdomain
self.no_prompt = no_prompt
self.available = False
self.key_required = False
self.login_response = {}
@property
def auth_header(self):
@@ -72,90 +131,105 @@ class Blink():
Method logs in and sets auth token, urls, and ids for future requests.
Essentially this is just a wrapper function for ease of use.
"""
if self._username is None or self._password is None:
self.login()
else:
if not self.available:
self.get_auth_token()
if self.key_required and not self.no_prompt:
email = self.login_handler.data["username"]
key = input("Enter code sent to {}: ".format(email))
result = self.login_handler.send_auth_key(self, key)
self.key_required = not result
self.setup_post_verify()
elif not self.key_required:
self.setup_post_verify()
def setup_post_verify(self):
"""Initialize blink system after verification."""
camera_list = self.get_cameras()
networks = self.get_ids()
for network_name, network_id in networks.items():
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()
self.sync[network_name] = sync_module
self.cameras = self.merge_cameras()
self.cameras = self.merge_cameras()
self.available = self.refresh()
self.key_required = False
def login(self):
"""Prompt user for username and password."""
self._username = input("Username:")
self._password = getpass.getpass("Password:")
if self.get_auth_token():
_LOGGER.info("Login successful!")
return True
_LOGGER.warning("Unable to login with %s.", self._username)
return False
"""Perform server login. DEPRECATED."""
_LOGGER.warning(
"Method is deprecated and will be removed in a future version. Please use the LoginHandler.login() method instead."
)
return self.login_handler.login(self)
def get_auth_token(self):
def get_auth_token(self, is_retry=False):
"""Retrieve the authentication token from Blink."""
if not isinstance(self._username, str):
raise BlinkAuthenticationException(ERROR.USERNAME)
if not isinstance(self._password, str):
raise BlinkAuthenticationException(ERROR.PASSWORD)
login_url = LOGIN_URL
self.session = create_session()
response = api.request_login(self,
login_url,
self._username,
self._password)
if response.status_code == 200:
response = response.json()
(self.region_id, self.region), = response['region'].items()
else:
_LOGGER.debug(
("Received response code %s "
"when authenticating, "
"trying new url"), response.status_code
)
login_url = LOGIN_BACKUP_URL
response = api.request_login(self,
login_url,
self._username,
self._password)
self.region_id = 'piri'
self.region = "UNKNOWN"
self._host = "{}.{}".format(self.region_id, BLINK_URL)
self._token = response['authtoken']['authtoken']
self._auth_header = {'Host': self._host,
'TOKEN_AUTH': self._token}
self.networks = response['networks']
self.urls = BlinkURLHandler(self.region_id)
self._login_url = login_url
self.login_response = self.login_handler.login(self)
if not self.login_response:
self.available = False
return False
self.setup_params(self.login_response)
if self.login_handler.check_key_required(self):
self.key_required = True
return self._auth_header
def setup_params(self, response):
"""Retrieve blink parameters from login response."""
self.login_url = self.login_handler.login_url
((self.region_id, self.region),) = response["region"].items()
self._host = "{}.{}".format(self.region_id, BLINK_URL)
self._token = response["authtoken"]["authtoken"]
self._auth_header = {"Host": self._host, "TOKEN_AUTH": self._token}
self.urls = BlinkURLHandler(self.region_id, legacy=self.legacy)
self.networks = self.get_networks()
self.client_id = response["client"]["id"]
self.account_id = response["account"]["id"]
def get_networks(self):
"""Get network information."""
response = api.request_networks(self)
try:
return response["summary"]
except KeyError:
return None
def get_ids(self):
"""Set the network ID and Account ID."""
response = api.request_networks(self)
# Look for only onboarded network, flag warning if multiple
# since it's unexpected
all_networks = []
network_dict = {}
for network, status in self.networks.items():
if status['onboarded']:
all_networks.append('{}'.format(network))
network_dict[status['name']] = network
# For the first onboarded network we find, grab the account id
for resp in response['networks']:
if str(resp['id']) in all_networks:
self.account_id = resp['account_id']
break
if status["onboarded"]:
all_networks.append("{}".format(network))
network_dict[status["name"]] = network
self.network_ids = all_networks
return network_dict
def get_cameras(self):
"""Retrieve a camera list for each onboarded network."""
response = api.request_homescreen(self)
try:
all_cameras = {}
for camera in response["cameras"]:
camera_network = str(camera["network_id"])
camera_name = camera["name"]
camera_id = camera["id"]
camera_info = {"name": camera_name, "id": camera_id}
if camera_network not in all_cameras:
all_cameras[camera_network] = []
all_cameras[camera_network].append(camera_info)
return all_cameras
except KeyError:
_LOGGER.error("Initialization failue. Could not retrieve cameras.")
return {}
@Throttle(seconds=MIN_THROTTLE_TIME)
def refresh(self, force_cache=False):
"""
Perform a system refresh.
@@ -166,6 +240,11 @@ class Blink():
for sync_name, sync_module in self.sync.items():
_LOGGER.debug("Attempting refresh of sync %s", sync_name)
sync_module.refresh(force_cache=force_cache)
if not force_cache:
# Prevents rapid clearing of motion detect property
self.last_refresh = int(time.time())
return True
return False
def check_if_ok_to_update(self):
"""Check if it is ok to perform an http request."""
@@ -174,7 +253,6 @@ class Blink():
if last_refresh is None:
last_refresh = 0
if current_time >= (last_refresh + self.refresh_rate):
self.last_refresh = current_time
return True
return False
@@ -184,3 +262,84 @@ class Blink():
for sync in self.sync:
combined = merge_dicts(combined, self.sync[sync].cameras)
return combined
def download_videos(self, path, since=None, camera="all", stop=10, debug=False):
"""
Download all videos from server since specified time.
:param path: Path to write files. /path/<cameraname>_<recorddate>.mp4
:param since: Date and time to get videos from.
Ex: "2018/07/28 12:33:00" to retrieve videos since
July 28th 2018 at 12:33:00
:param camera: Camera name to retrieve. Defaults to "all".
Use a list for multiple cameras.
:param stop: Page to stop on (~25 items per page. Default page 10).
:param debug: Set to TRUE to prevent downloading of items.
Instead of downloading, entries will be printed to log.
"""
if since is None:
since_epochs = self.last_refresh
else:
parsed_datetime = parse(since, fuzzy=True)
since_epochs = parsed_datetime.timestamp()
formatted_date = get_time(time_to_convert=since_epochs)
_LOGGER.info("Retrieving videos since %s", formatted_date)
if not isinstance(camera, list):
camera = [camera]
for page in range(1, stop):
response = api.request_videos(self, time=since_epochs, page=page)
_LOGGER.debug("Processing page %s", page)
try:
result = response["media"]
if not result:
raise IndexError
except (KeyError, IndexError):
_LOGGER.info("No videos found on page %s. Exiting.", page)
break
self._parse_downloaded_items(result, camera, path, debug)
def _parse_downloaded_items(self, result, camera, path, debug):
"""Parse downloaded videos."""
for item in result:
try:
created_at = item["created_at"]
camera_name = item["device_name"]
is_deleted = item["deleted"]
address = item["media"]
except KeyError:
_LOGGER.info("Missing clip information, skipping...")
continue
if camera_name not in camera and "all" not in camera:
_LOGGER.debug("Skipping videos for %s.", camera_name)
continue
if is_deleted:
_LOGGER.debug("%s: %s is marked as deleted.", camera_name, address)
continue
clip_address = "{}{}".format(self.urls.base_url, address)
filename = "{}-{}".format(camera_name, created_at)
filename = "{}.mp4".format(slugify(filename))
filename = os.path.join(path, filename)
if not debug:
if os.path.isfile(filename):
_LOGGER.info("%s already exists, skipping...", filename)
continue
response = api.http_get(self, url=clip_address, stream=True, json=False)
with open(filename, "wb") as vidfile:
copyfileobj(response.raw, vidfile)
_LOGGER.info("Downloaded video to %s", filename)
else:
print(
("Camera: {}, Timestamp: {}, " "Address: {}, Filename: {}").format(
camera_name, created_at, address, filename
)
)
+77 -97
View File
@@ -6,10 +6,8 @@ from blinkpy import api
_LOGGER = logging.getLogger(__name__)
MAX_CLIPS = 5
class BlinkCamera():
class BlinkCamera:
"""Class to initialize individual camera."""
def __init__(self, sync):
@@ -24,10 +22,11 @@ class BlinkCamera():
self.battery_voltage = None
self.clip = None
self.temperature = None
self.temperature_calibrated = None
self.battery_state = None
self.motion_detected = None
self.wifi_strength = None
self.last_record = []
self.last_record = None
self._cached_image = None
self._cached_video = None
@@ -35,27 +34,29 @@ class BlinkCamera():
def attributes(self):
"""Return dictionary of all camera attributes."""
attributes = {
'name': self.name,
'camera_id': self.camera_id,
'serial': self.serial,
'temperature': self.temperature,
'temperature_c': self.temperature_c,
'battery': self.battery,
'thumbnail': self.thumbnail,
'video': self.clip,
'motion_enabled': self.motion_enabled,
'motion_detected': self.motion_detected,
'wifi_strength': self.wifi_strength,
'network_id': self.sync.network_id,
'sync_module': self.sync.name,
'last_record': self.last_record
"name": self.name,
"camera_id": self.camera_id,
"serial": self.serial,
"temperature": self.temperature,
"temperature_c": self.temperature_c,
"temperature_calibrated": self.temperature_calibrated,
"battery": self.battery,
"battery_voltage": self.battery_voltage,
"thumbnail": self.thumbnail,
"video": self.clip,
"motion_enabled": self.motion_enabled,
"motion_detected": self.motion_detected,
"wifi_strength": self.wifi_strength,
"network_id": self.sync.network_id,
"sync_module": self.sync.name,
"last_record": self.last_record,
}
return attributes
@property
def battery(self):
"""Return battery level as percentage."""
return round(self.battery_voltage / 180 * 100)
"""Return battery as string."""
return self.battery_state
@property
def temperature_c(self):
@@ -78,53 +79,66 @@ class BlinkCamera():
def snap_picture(self):
"""Take a picture with camera to create a new thumbnail."""
return api.request_new_image(self.sync.blink,
self.network_id,
self.camera_id)
return api.request_new_image(self.sync.blink, self.network_id, self.camera_id)
def set_motion_detect(self, enable):
"""Set motion detection."""
if enable:
return api.request_motion_detection_enable(self.sync.blink,
self.network_id,
self.camera_id)
return api.request_motion_detection_disable(self.sync.blink,
self.network_id,
self.camera_id)
return api.request_motion_detection_enable(
self.sync.blink, self.network_id, self.camera_id
)
return api.request_motion_detection_disable(
self.sync.blink, self.network_id, self.camera_id
)
def update(self, config, force_cache=False):
def update(self, config, force_cache=False, **kwargs):
"""Update camera info."""
self.name = config['name']
self.camera_id = str(config['camera_id'])
self.network_id = str(config['network_id'])
self.serial = config['serial']
self.motion_enabled = config['enabled']
self.battery_voltage = config['battery_voltage']
self.battery_state = config['battery_state']
self.temperature = config['temperature']
self.wifi_strength = config['wifi_strength']
# force = kwargs.pop('force', False)
self.name = config["name"]
self.camera_id = str(config["id"])
self.network_id = str(config["network_id"])
self.serial = config["serial"]
self.motion_enabled = config["enabled"]
self.battery_voltage = config["battery_voltage"]
self.battery_state = config["battery_state"]
self.temperature = config["temperature"]
self.wifi_strength = config["wifi_strength"]
# Retrieve calibrated temperature from special endpoint
resp = api.request_camera_sensors(
self.sync.blink, self.network_id, self.camera_id
)
try:
self.temperature_calibrated = resp["temp"]
except KeyError:
self.temperature_calibrated = self.temperature
_LOGGER.warning("Could not retrieve calibrated temperature.")
# Check if thumbnail exists in config, if not try to
# get it from the homescreen info in teh sync module
# get it from the homescreen info in the sync module
# otherwise set it to None and log an error
new_thumbnail = None
if config['thumbnail']:
thumb_addr = config['thumbnail']
thumb_addr = None
if config["thumbnail"]:
thumb_addr = config["thumbnail"]
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:
new_thumbnail = "{}{}.jpg".format(self.sync.urls.base_url,
thumb_addr)
new_thumbnail = "{}{}.jpg".format(self.sync.urls.base_url, thumb_addr)
try:
self.motion_detected = self.sync.motion[self.name]
except KeyError:
self.motion_detected = False
# Check if a new motion clip has been recorded
# check_for_motion_method sets motion_detected variable
self.check_for_motion()
clip_addr = None
if self.last_record:
clip_addr = self.sync.all_clips[self.name][self.last_record[0]]
self.clip = "{}{}".format(self.sync.urls.base_url,
clip_addr)
if self.name in self.sync.last_record:
clip_addr = self.sync.last_record[self.name]["clip"]
self.last_record = self.sync.last_record[self.name]["time"]
self.clip = "{}{}".format(self.sync.urls.base_url, clip_addr)
# If the thumbnail or clip have changed, update the cache
update_cached_image = False
@@ -137,34 +151,13 @@ class BlinkCamera():
update_cached_video = True
if new_thumbnail is not None and (update_cached_image or force_cache):
self._cached_image = api.http_get(self.sync.blink,
url=self.thumbnail,
stream=True,
json=False)
self._cached_image = api.http_get(
self.sync.blink, url=self.thumbnail, stream=True, json=False
)
if clip_addr is not None and (update_cached_video or force_cache):
self._cached_video = api.http_get(self.sync.blink,
url=self.clip,
stream=True,
json=False)
def 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)
self._cached_video = api.http_get(
self.sync.blink, url=self.clip, stream=True, json=False
)
def image_to_file(self, path):
"""
@@ -175,11 +168,12 @@ class BlinkCamera():
_LOGGER.debug("Writing image from %s to %s", self.name, path)
response = self._cached_image
if response.status_code == 200:
with open(path, 'wb') as imgfile:
with open(path, "wb") as imgfile:
copyfileobj(response.raw, imgfile)
else:
_LOGGER.error("Cannot write image to file, response %s",
response.status_code)
_LOGGER.error(
"Cannot write image to file, response %s", response.status_code
)
def video_to_file(self, path):
"""Write video to file.
@@ -191,19 +185,5 @@ class BlinkCamera():
if response is None:
_LOGGER.error("No saved video exist for %s.", self.name)
return
with open(path, 'wb') as vidfile:
with open(path, "wb") as 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
+53 -39
View File
@@ -3,54 +3,68 @@
import os
MAJOR_VERSION = 0
MINOR_VERSION = 11
PATCH_VERSION = 0
MINOR_VERSION = 15
PATCH_VERSION = "0-rc.1"
__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
__version__ = "{}.{}.{}".format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3)
PROJECT_NAME = 'blinkpy'
PROJECT_PACKAGE_NAME = 'blinkpy'
PROJECT_LICENSE = 'MIT'
PROJECT_AUTHOR = 'Kevin Fronczak'
PROJECT_COPYRIGHT = ' 2017, {}'.format(PROJECT_AUTHOR)
PROJECT_URL = 'https://github.com/fronzbot/blinkpy'
PROJECT_EMAIL = 'kfronczak@gmail.com'
PROJECT_DESCRIPTION = ('A Blink camera Python library '
'running on Python 3.')
PROJECT_LONG_DESCRIPTION = ('blinkpy is an open-source '
'unofficial API for the Blink Camera '
'system with the intention for easy '
'integration into various home '
'automation platforms.')
if os.path.exists('README.rst'):
PROJECT_LONG_DESCRIPTION = open('README.rst').read()
PROJECT_NAME = "blinkpy"
PROJECT_PACKAGE_NAME = "blinkpy"
PROJECT_LICENSE = "MIT"
PROJECT_AUTHOR = "Kevin Fronczak"
PROJECT_COPYRIGHT = " 2017, {}".format(PROJECT_AUTHOR)
PROJECT_URL = "https://github.com/fronzbot/blinkpy"
PROJECT_EMAIL = "kfronczak@gmail.com"
PROJECT_DESCRIPTION = "A Blink camera Python library " "running on Python 3."
PROJECT_LONG_DESCRIPTION = (
"blinkpy is an open-source "
"unofficial API for the Blink Camera "
"system with the intention for easy "
"integration into various home "
"automation platforms."
)
if os.path.exists("README.rst"):
PROJECT_LONG_DESCRIPTION = open("README.rst").read()
PROJECT_CLASSIFIERS = [
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Home Automation'
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Topic :: Home Automation",
]
PROJECT_GITHUB_USERNAME = 'fronzbot'
PROJECT_GITHUB_REPOSITORY = 'blinkpy'
PROJECT_GITHUB_USERNAME = "fronzbot"
PROJECT_GITHUB_REPOSITORY = "blinkpy"
PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME)
PYPI_URL = "https://pypi.python.org/pypi/{}".format(PROJECT_PACKAGE_NAME)
'''
"""
URLS
'''
BLINK_URL = 'immedia-semi.com'
LOGIN_URL = 'https://prod.' + BLINK_URL + '/login'
LOGIN_BACKUP_URL = 'https://rest.piri/' + BLINK_URL + '/login'
BASE_URL = 'https://prod.' + BLINK_URL
DEFAULT_URL = 'prod.' + BLINK_URL
"""
BLINK_URL = "immedia-semi.com"
DEFAULT_URL = "{}.{}".format("rest-prod", BLINK_URL)
BASE_URL = "https://{}".format(DEFAULT_URL)
LOGIN_URLS = [
"{}/api/v4/login".format(BASE_URL),
"{}/api/v3/login".format(BASE_URL),
"{}/api/v2/login".format(BASE_URL),
]
'''
"""
Dictionaries
'''
ONLINE = {'online': True, 'offline': False}
"""
ONLINE = {"online": True, "offline": False}
"""
OTHER
"""
TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
DEFAULT_MOTION_INTERVAL = 1
DEFAULT_REFRESH = 30
MIN_THROTTLE_TIME = 2
SIZE_NOTIFICATION_KEY = 152
SIZE_UID = 16
+4 -2
View File
@@ -4,10 +4,12 @@ USERNAME = (0, "Username must be a string")
PASSWORD = (1, "Password must be a string")
AUTHENTICATE = (
2,
"Cannot authenticate since either password or username has not been set"
"Cannot authenticate since either password or username has not been set",
)
AUTH_TOKEN = (
3,
"Authentication header incorrect. Are you sure you received your token?"
"Authentication header incorrect. Are you sure you received your token?",
)
REQUEST = (4, "Cannot perform request (get/post type incorrect)")
BLINK_ERRORS = [101, 400, 404]
+123 -27
View File
@@ -1,38 +1,82 @@
"""Useful functions for blinkpy."""
import logging
import time
import secrets
from calendar import timegm
from functools import partial, wraps
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
_LOGGER = logging.getLogger(__name__)
def gen_uid(size):
"""Create a random sring."""
full_token = secrets.token_hex(size)
return full_token[0:size]
def time_to_seconds(timestamp):
"""Convert TIMESTAMP_FORMAT time to seconds."""
try:
dtime = dateutil.parser.isoparse(timestamp)
except ValueError:
_LOGGER.error("Incorrect timestamp format for conversion: %s.", timestamp)
return False
return timegm(dtime.timetuple())
def get_time(time_to_convert=None):
"""Create blink-compatible timestamp."""
if time_to_convert is None:
time_to_convert = time.time()
return time.strftime(TIMESTAMP_FORMAT, time.gmtime(time_to_convert))
def merge_dicts(dict_a, dict_b):
"""Merge two dictionaries into one."""
duplicates = [val for val in dict_a if val in dict_b]
if duplicates:
_LOGGER.warning(("Duplicates found during merge: %s. "
"Renaming is recommended."), duplicates)
_LOGGER.warning(
("Duplicates found during merge: %s. " "Renaming is recommended."),
duplicates,
)
return {**dict_a, **dict_b}
def create_session():
"""Create a session for blink communication."""
"""
Create a session for blink communication.
From @ericfrederich via
https://github.com/kennethreitz/requests/issues/2011
"""
sess = Session()
sess.get = partial(sess.get, timeout=5)
return sess
def attempt_reauthorization(blink):
"""Attempt to refresh auth token and links."""
_LOGGER.info("Auth token expired, attempting reauthorization.")
headers = blink.get_auth_token()
headers = blink.get_auth_token(is_retry=True)
return headers
def http_req(blink, url='http://example.com', data=None, headers=None,
reqtype='get', stream=False, json_resp=True, is_retry=False):
def http_req(
blink,
url="http://example.com",
data=None,
headers=None,
reqtype="get",
stream=False,
json_resp=True,
is_retry=False,
):
"""
Perform server requests and check if reauthorization neccessary.
@@ -45,10 +89,10 @@ def http_req(blink, url='http://example.com', data=None, headers=None,
:param json_resp: Return JSON response? TRUE/False
:param is_retry: Is this a retry attempt? True/FALSE
"""
if reqtype == 'post':
req = Request('POST', url, headers=headers, data=data)
elif reqtype == 'get':
req = Request('GET', url, headers=headers)
if reqtype == "post":
req = Request("POST", url, headers=headers, data=data)
elif reqtype == "get":
req = Request("GET", url, headers=headers)
else:
_LOGGER.error("Invalid request type: %s", reqtype)
raise BlinkException(ERROR.REQUEST)
@@ -57,24 +101,44 @@ def http_req(blink, url='http://example.com', data=None, headers=None,
try:
response = blink.session.send(prepped, stream=stream)
if json_resp and 'code' in response.json():
if is_retry:
if json_resp and "code" in response.json():
resp_dict = response.json()
code = resp_dict["code"]
message = resp_dict["message"]
if is_retry and code in ERROR.BLINK_ERRORS:
_LOGGER.error("Cannot obtain new token for server auth.")
return None
else:
elif code in ERROR.BLINK_ERRORS:
headers = attempt_reauthorization(blink)
return http_req(blink, url=url, data=data, headers=headers,
reqtype=reqtype, stream=stream,
json_resp=json_resp, is_retry=True)
if not headers:
raise exceptions.ConnectionError
return http_req(
blink,
url=url,
data=data,
headers=headers,
reqtype=reqtype,
stream=stream,
json_resp=json_resp,
is_retry=True,
)
_LOGGER.warning("Response from server: %s - %s", code, message)
except (exceptions.ConnectionError, exceptions.Timeout):
_LOGGER.info("Cannot connect to server with url %s.", url)
if not is_retry:
headers = attempt_reauthorization(blink)
return http_req(blink, url=url, data=data, headers=headers,
reqtype=reqtype, stream=stream,
json_resp=json_resp, is_retry=True)
_LOGGER.error("Endpoint %s failed. Possible issue with Blink servers.",
url)
return http_req(
blink,
url=url,
data=data,
headers=headers,
reqtype=reqtype,
stream=stream,
json_resp=json_resp,
is_retry=True,
)
_LOGGER.error("Endpoint %s failed. Possible issue with Blink servers.", url)
return None
if json_resp:
@@ -96,18 +160,50 @@ class BlinkException(Exception):
class BlinkAuthenticationException(BlinkException):
"""Class to throw authentication exception."""
pass
class BlinkURLHandler():
class BlinkURLHandler:
"""Class that handles Blink URLS."""
def __init__(self, region_id):
def __init__(self, region_id, legacy=False):
"""Initialize the urls."""
self.base_url = "https://rest.{}.{}".format(region_id, BLINK_URL)
self.subdomain = "rest-{}".format(region_id)
if legacy:
self.subdomain = "rest.{}".format(region_id)
self.base_url = "https://{}.{}".format(self.subdomain, BLINK_URL)
self.home_url = "{}/homescreen".format(self.base_url)
self.event_url = "{}/events/network".format(self.base_url)
self.network_url = "{}/network".format(self.base_url)
self.networks_url = "{}/networks".format(self.base_url)
self.video_url = "{}/api/v2/videos".format(self.base_url)
_LOGGER.debug("Setting base url to %s.", self.base_url)
class Throttle:
"""Class for throttling api calls."""
def __init__(self, seconds=10):
"""Initialize throttle class."""
self.throttle_time = seconds
self.last_call = 0
def __call__(self, method):
"""Throttle caller method."""
def throttle_method():
"""Call when method is throttled."""
return None
@wraps(method)
def wrapper(*args, **kwargs):
"""Wrap that checks for throttling."""
force = kwargs.pop("force", False)
now = int(time.time())
last_call_delta = now - self.last_call
if force or last_call_delta > self.throttle_time:
result = method(*args, *kwargs)
self.last_call = now
return result
return throttle_method()
return wrapper
+154
View File
@@ -0,0 +1,154 @@
"""Login handler for blink."""
import json
import logging
from os.path import isfile
from getpass import getpass
from blinkpy import api
from blinkpy.helpers import util
from blinkpy.helpers import constants as const
_LOGGER = logging.getLogger(__name__)
class LoginHandler:
"""Class to handle login communication."""
def __init__(
self,
username=None,
password=None,
cred_file=None,
persist_key=None,
device_id="Blinkpy",
):
"""
Initialize login handler.
:param username: Blink username
:param password: Blink password
:param cred_file: JSON formatted credential file.
:param persist_key: File location of persistant key.
:param device_id: Name of application to send at login.
"""
self.login_url = None
self.login_urls = const.LOGIN_URLS
self.cred_file = cred_file
self.persist_key = persist_key
self.device_id = device_id
self.data = {
"username": username,
"password": password,
"uid": None,
"notification_key": None,
}
self.check_keys()
def check_keys(self):
"""Check if uid exists, if not create."""
uid = util.gen_uid(const.SIZE_UID)
notification_key = util.gen_uid(const.SIZE_NOTIFICATION_KEY)
data = {"uid": uid, "notification_key": notification_key}
if self.persist_key is None:
return data
if not isfile(self.persist_key):
with open(self.persist_key, "w") as json_file:
json.dump(data, json_file)
else:
with open(self.persist_key, "r") as json_file:
data = json.load(json_file)
return data
def check_cred_file(self):
"""Check if credential file supplied and use if so."""
if isfile(self.cred_file):
try:
with open(self.cred_file, "r") as json_file:
creds = json.load(json_file)
self.data["username"] = creds["username"]
self.data["password"] = creds["password"]
except ValueError:
_LOGGER.error(
"Improperly formatted json file %s.", self.cred_file, exc_info=True
)
return False
except KeyError:
_LOGGER.error("JSON file information incomplete %s.", exc_info=True)
return False
return True
return False
def check_login(self):
"""Check login information and prompt if not available."""
if self.data["username"] is None:
self.data["username"] = input("Username:")
if self.data["password"] is None:
self.data["password"] = getpass("Password:")
if self.data["username"] and self.data["password"]:
return True
return False
def validate_response(self, url, response):
"""Validate response from login endpoint."""
try:
if response.status_code != 200:
return False
except AttributeError:
_LOGGER.error(
"Response for %s did not return a status code. Deprecated endpoint?",
url,
)
return False
return True
def login(self, blink):
"""Attempt login to blink servers."""
if self.cred_file is not None:
self.check_cred_file()
if not self.check_login():
_LOGGER.error("Cannot login with username %s", self.data["username"])
return False
for url in self.login_urls:
_LOGGER.info("Attempting login with %s", url)
response = api.request_login(
blink,
url,
self.data["username"],
self.data["password"],
self.data["notification_key"],
self.data["uid"],
is_retry=False,
device_id=self.device_id,
)
if self.validate_response(url, response):
self.login_url = url
return response.json()
_LOGGER.error("Failed to login to Blink servers. Last response: %s", response)
return False
def send_auth_key(self, blink, key):
"""Send 2FA key to blink servers."""
if key is not None:
response = api.request_verify(blink, key)
try:
json_resp = response.json()
blink.available = json_resp["valid"]
except (KeyError, TypeError):
_LOGGER.error("Did not receive valid response from server.")
return False
return True
def check_key_required(self, blink):
"""Check if 2FA key is required."""
try:
if blink.login_response["client"]["verification_required"]:
return True
except KeyError:
pass
return False
+121 -98
View File
@@ -5,15 +5,16 @@ import logging
from requests.structures import CaseInsensitiveDict
from blinkpy import api
from blinkpy.camera import BlinkCamera
from blinkpy.helpers.util import time_to_seconds
from blinkpy.helpers.constants import ONLINE
_LOGGER = logging.getLogger(__name__)
class BlinkSyncModule():
class BlinkSyncModule:
"""Class to initialize sync module."""
def __init__(self, blink, network_name, network_id):
def __init__(self, blink, network_name, network_id, camera_list):
"""
Initialize Blink sync module.
@@ -30,25 +31,26 @@ class BlinkSyncModule():
self.sync_id = None
self.host = None
self.summary = None
self.homescreen = None
self.network_info = None
self.record_dates = {}
self.videos = {}
self.events = []
self.cameras = CaseInsensitiveDict({})
self.all_clips = {}
self.motion_interval = blink.motion_interval
self.motion = {}
self.last_record = {}
self.camera_list = camera_list
self.available = False
@property
def attributes(self):
"""Return sync attributes."""
attr = {
'name': self.name,
'id': self.sync_id,
'network_id': self.network_id,
'serial': self.serial,
'status': self.status,
'region': self.region,
'region_id': self.region_id,
"name": self.name,
"id": self.sync_id,
"network_id": self.network_id,
"serial": self.serial,
"status": self.status,
"region": self.region,
"region_id": self.region_id,
}
return attr
@@ -65,7 +67,10 @@ class BlinkSyncModule():
@property
def arm(self):
"""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
def arm(self, value):
@@ -78,101 +83,119 @@ class BlinkSyncModule():
def start(self):
"""Initialize the system."""
response = api.request_syncmodule(self.blink, self.network_id)
self.summary = response['syncmodule']
self.sync_id = self.summary['id']
self.network_id = self.summary['network_id']
self.serial = self.summary['serial']
self.status = self.summary['status']
try:
self.summary = response["syncmodule"]
self.network_id = self.summary["network_id"]
except (TypeError, KeyError):
_LOGGER.error(
("Could not retrieve sync module information " "with response: %s"),
response,
exc_info=True,
)
return False
self.events = self.get_events()
try:
self.sync_id = self.summary["id"]
self.serial = self.summary["serial"]
self.status = self.summary["status"]
except KeyError:
_LOGGER.error(
"Could not extract some sync module info: %s", response, exc_info=True
)
self.homescreen = api.request_homescreen(self.blink)
self.get_network_info()
self.check_new_videos()
try:
for camera_config in self.camera_list:
if "name" not in camera_config:
break
name = camera_config["name"]
self.cameras[name] = BlinkCamera(self)
self.motion[name] = False
camera_info = self.get_camera_info(camera_config["id"])
self.cameras[name].update(camera_info, force_cache=True, force=True)
except KeyError:
_LOGGER.error(
"Could not create cameras instances for %s", self.name, exc_info=True
)
return False
self.network_info = api.request_network_status(self.blink,
self.network_id)
self.available = True
return True
camera_info = self.get_camera_info()
for camera_config in camera_info:
name = camera_config['name']
self.cameras[name] = BlinkCamera(self)
self.videos = self.get_videos()
for camera_config in camera_info:
name = camera_config['name']
if name in self.cameras:
self.cameras[name].update(camera_config, force_cache=True)
def get_events(self):
def get_events(self, **kwargs):
"""Retrieve events from server."""
response = api.request_sync_events(self.blink, self.network_id)
return response['event']
force = kwargs.pop("force", False)
response = api.request_sync_events(self.blink, self.network_id, force=force)
try:
return response["event"]
except (TypeError, KeyError):
_LOGGER.error("Could not extract events: %s", response, exc_info=True)
return False
def get_camera_info(self):
def get_camera_info(self, camera_id):
"""Retrieve camera information."""
response = api.request_cameras(self.blink, self.network_id)
return response['devicestatus']
response = api.request_camera_info(self.blink, self.network_id, camera_id)
try:
return response["camera"][0]
except (TypeError, KeyError):
_LOGGER.error("Could not extract camera info: %s", response, exc_info=True)
return []
def get_network_info(self):
"""Retrieve network status."""
is_errored = False
self.network_info = api.request_network_status(self.blink, self.network_id)
try:
is_errored = self.network_info["network"]["sync_module_error"]
except KeyError:
is_errored = True
if is_errored:
self.available = False
def refresh(self, force_cache=False):
"""Get all blink cameras and pulls their most recent status."""
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_id)
camera_info = self.get_camera_info()
for camera_config in camera_info:
name = camera_config['name']
self.cameras[name].update(camera_config, force_cache=force_cache)
self.get_network_info()
self.check_new_videos()
for camera_name in self.cameras.keys():
camera_id = self.cameras[camera_name].camera_id
camera_info = self.get_camera_info(camera_id)
self.cameras[camera_name].update(camera_info, force_cache=force_cache)
def get_videos(self, start_page=0, end_page=1):
"""
Retrieve last recorded videos per camera.
def check_new_videos(self):
"""Check if new videos since last refresh."""
try:
interval = self.blink.last_refresh - self.motion_interval * 60
except TypeError:
# This is the first start, so refresh hasn't happened yet.
# No need to check for motion.
return False
:param start_page: Page to start reading from on blink servers
(defaults to 0)
:param end_page: Page to stop reading from (defaults to 1)
"""
videos = list()
all_dates = dict()
resp = api.request_videos(self.blink, time=interval, page=1)
for page_num in range(start_page, end_page + 1):
this_page = api.request_videos(self.blink, page=page_num)
if not this_page:
break
videos.append(this_page)
_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}
for camera in self.cameras.keys():
self.motion[camera] = False
if camera_name not in all_dates:
all_dates[camera_name] = list()
all_dates[camera_name].append(clip_date)
try:
self.videos[camera_name].append(
{
'clip': clip_addr,
'thumb': thumb_addr,
}
)
except KeyError:
self.videos[camera_name] = [
{
'clip': clip_addr,
'thumb': thumb_addr,
}
]
self.record_dates = all_dates
_LOGGER.debug("Retrieved a total of %s records", len(all_dates))
return self.videos
try:
info = resp["media"]
except (KeyError, TypeError):
_LOGGER.warning("Could not check for motion. Response: %s", resp)
return False
for entry in info:
try:
name = entry["device_name"]
clip = entry["media"]
timestamp = entry["created_at"]
if self.check_new_video_time(timestamp):
self.motion[name] = True and self.arm
self.last_record[name] = {"clip": clip, "time": timestamp}
except KeyError:
_LOGGER.debug("No new videos since last refresh.")
return True
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: 2%
comment: true
require_ci_to_pass: yes
range: 65..90
round: down
precision: 1
+7
View File
@@ -8,11 +8,16 @@ reports=no
# too-many-* - are not enforced for the sake of readability
# too-few-* - same as too-many-*
# no-else-return - I don't see any reason to enforce this. both forms are readable
# no-self-use - stupid and only annoying
# unexpected-keyword-arg - doesn't allow for use of **kwargs, which is dumb
disable=
format,
bad-continuation,
locally-disabled,
unused-argument,
duplicate-code,
implicit-str-concat,
too-many-arguments,
too-many-branches,
too-many-instance-attributes,
@@ -23,3 +28,5 @@ disable=
too-many-lines,
too-few-public-methods,
no-else-return,
no-self-use,
unexpected-keyword-arg,
+4 -2
View File
@@ -1,2 +1,4 @@
requests>=2.20.0
testtools==2.3.0
python-dateutil~=2.8.1
requests~=2.23.0
python-slugify~=4.0.0
testtools==2.4.0
+10 -8
View File
@@ -1,10 +1,12 @@
flake8==3.5.0
flake8-docstrings==1.3.0
pylint==2.1.1
pydocstyle==2.1.1
pytest==3.7.1
pytest-cov>=2.3.1
pytest-sugar>=0.9.0
pytest-timeout>=1.0.0
black==19.10b0
coverage==5.1
flake8==3.7.9
flake8-docstrings==1.5.0
pylint==2.5.2
pydocstyle==5.0.2
pytest==5.4.1
pytest-cov==2.8.1
pytest-sugar>=0.9.2
pytest-timeout>=1.3.3
restructuredtext-lint>=1.0.1
pygments>=2.2.0
+18
View File
@@ -0,0 +1,18 @@
[flake8]
exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build
doctests = True
# To work with Black
max-line-length = 88
# E501: line too long
# W503: Line break occurred before a binary operator
# E203: Whitespace before ':'
# D202 No blank lines allowed after function docstring
# W504 line break after binary operator
ignore =
E501,
W503,
E203,
D202,
W504,
+26 -33
View File
@@ -1,48 +1,41 @@
"""Setup file for blinkpy."""
# -*- coding: utf-8 -*-
from os.path import abspath, dirname, join
from os.path import abspath, dirname
from setuptools import setup, find_packages
# Fixes import error due to breaking change in pip 10
# https://stackoverflow.com/a/49867265
# pip > 10
try:
from pip._internal.req import parse_requirements
# pip < 10
except ImportError:
from pip.req import parse_requirements
from blinkpy.helpers.constants import (
__version__, PROJECT_PACKAGE_NAME, PROJECT_LICENSE, PROJECT_URL,
PROJECT_EMAIL, PROJECT_DESCRIPTION, PROJECT_CLASSIFIERS, PROJECT_AUTHOR,
)
this_dir = abspath(dirname(__file__))
PROJECT_VERSION = __version__
install_reqs = parse_requirements('{}/requirements.txt'.format(this_dir), session=False)
test_reqs = parse_requirements('{}/requirements_test.txt'.format(this_dir), session=False)
REQUIRES = [str(ir.req) for ir in install_reqs]
TEST_REQUIRES = [str(ir.req) for ir in test_reqs]
THIS_DIR = abspath(dirname(__file__))
REQUIRES = [
"python-dateutil~=2.8.1",
"requests~=2.23.0",
"python-slugify~=4.0.0",
]
PACKAGES = find_packages(exclude=['tests*', 'docs'])
with open('{}/README.rst'.format(this_dir), encoding='utf-8') as readme_file:
long_description = readme_file.read()
name = PROJECT_PACKAGE_NAME
version = __version__
with open('{}/README.rst'.format(THIS_DIR), encoding='utf-8') as readme_file:
LONG_DESCRIPTION = readme_file.read()
setup(
name = PROJECT_PACKAGE_NAME,
version = __version__,
description = PROJECT_DESCRIPTION,
long_description = long_description,
author = PROJECT_AUTHOR,
author_email = PROJECT_EMAIL,
license = PROJECT_LICENSE,
url = PROJECT_URL,
platforms = 'any',
py_modules = ['blinkpy'],
name=PROJECT_PACKAGE_NAME,
version=PROJECT_VERSION,
description=PROJECT_DESCRIPTION,
long_description=LONG_DESCRIPTION,
author=PROJECT_AUTHOR,
author_email=PROJECT_EMAIL,
license=PROJECT_LICENSE,
url=PROJECT_URL,
platforms='any',
py_modules=['blinkpy'],
packages=PACKAGES,
include_package_data = True,
install_requires = REQUIRES,
test_suite = 'tests',
classifiers = PROJECT_CLASSIFIERS
include_package_data=True,
install_requires=REQUIRES,
test_suite='tests',
classifiers=PROJECT_CLASSIFIERS
)
+19 -21
View File
@@ -4,11 +4,11 @@ from blinkpy.helpers.util import BlinkURLHandler
import blinkpy.helpers.constants as const
LOGIN_RESPONSE = {
'region': {'mock': 'Test'},
'networks': {
'1234': {'name': 'test', 'onboarded': True}
},
'authtoken': {'authtoken': 'foobar123', 'message': 'auth'}
"region": {"mock": "Test"},
"networks": {"1234": {"name": "test", "onboarded": True}},
"authtoken": {"authtoken": "foobar123", "message": "auth"},
"client": {"id": "5678"},
"account": {"id": "1337"},
}
@@ -37,29 +37,29 @@ def mocked_session_send(*args, **kwargs):
url = prepped.url
header = prepped.headers
method = prepped.method
if method == 'GET':
expected_token = LOGIN_RESPONSE['authtoken']['authtoken']
if header['TOKEN_AUTH'] != expected_token:
response = {'message': 'Not Authorized', 'code': 400}
if method == "GET":
expected_token = LOGIN_RESPONSE["authtoken"]["authtoken"]
if header["TOKEN_AUTH"] != expected_token:
response = {"message": "Not Authorized", "code": 400}
status = 400
elif url == 'use_bad_response':
response = {'foo': 'bar'}
elif url == "use_bad_response":
response = {"foo": "bar"}
status = 200
elif url == 'reauth':
response = {'message': 'REAUTH', 'code': 777}
elif url == "reauth":
response = {"message": "REAUTH", "code": 777}
status = 777
else:
response = {'test': 'foo'}
response = {"test": "foo"}
status = 200
elif method == 'POST':
if url in (const.LOGIN_URL, const.LOGIN_BACKUP_URL):
elif method == "POST":
if url in const.LOGIN_URLS:
response = LOGIN_RESPONSE
status = 200
elif url == 'http://wrong.url/' or url is None:
response = {'message': 'Error', 'code': 404}
elif url == "http://wrong.url/" or url is None:
response = {"message": "Error", "code": 404}
status = 404
else:
response = {'message': 'foo', 'code': 200}
response = {"message": "foo", "code": 200}
status = 200
return MockResponse(response, status)
@@ -67,5 +67,3 @@ def mocked_session_send(*args, **kwargs):
class MockURLHandler(BlinkURLHandler):
"""Mocks URL Handler in blinkpy module."""
pass
-42
View File
@@ -1,42 +0,0 @@
"""Test various api functions."""
import unittest
from unittest import mock
from blinkpy import api
from blinkpy.blinkpy import Blink
from blinkpy.helpers.util import create_session
class TestBlinkAPI(unittest.TestCase):
"""Test the blink api module."""
def setUp(self):
"""Initialize the blink module."""
self.blink = Blink()
self.blink.session = create_session()
# pylint: disable=protected-access
self.blink._auth_header = {}
def tearDown(self):
"""Tear down blink module."""
self.blink = None
@mock.patch('blinkpy.blinkpy.Blink.get_auth_token')
def test_http_req_connect_error(self, mock_auth):
"""Test http_get error condition."""
mock_auth.return_value = {'foo': 'bar'}
firstlog = ("INFO:blinkpy.helpers.util:"
"Cannot connect to server with url {}.").format(
'http://notreal.fake')
nextlog = ("INFO:blinkpy.helpers.util:"
"Auth token expired, attempting reauthorization.")
lastlog = ("ERROR:blinkpy.helpers.util:"
"Endpoint {} failed. Possible issue with "
"Blink servers.").format('http://notreal.fake')
expected = [firstlog, nextlog, firstlog, lastlog]
with self.assertLogs() as getlog:
api.http_get(self.blink, 'http://notreal.fake')
with self.assertLogs() as postlog:
api.http_post(self.blink, 'http://notreal.fake')
self.assertEqual(getlog.output, expected)
self.assertEqual(postlog.output, expected)
+100 -34
View File
@@ -1,16 +1,15 @@
"""Tests camera and system functions."""
import unittest
from unittest import mock
from requests import Request
import logging
from blinkpy import blinkpy
from blinkpy.sync_module import BlinkSyncModule
from blinkpy.helpers.util import create_session
from blinkpy.helpers.util import create_session, get_time
import tests.mock_responses as mresp
USERNAME = 'foobar'
PASSWORD = 'deadbeef'
USERNAME = "foobar"
PASSWORD = "deadbeef"
class MockSyncModule(BlinkSyncModule):
@@ -18,7 +17,7 @@ class MockSyncModule(BlinkSyncModule):
def __init__(self, blink, header):
"""Create mock sync module instance."""
super().__init__(blink, header)
super().__init__(blink, header, network_id=None, camera_list=None)
self.blink = blink
self.header = header
self.return_value = None
@@ -35,50 +34,117 @@ class MockSyncModule(BlinkSyncModule):
return self.return_value
@mock.patch('blinkpy.helpers.util.Session.send',
side_effect=mresp.mocked_session_send)
@mock.patch("blinkpy.helpers.util.Session.send", side_effect=mresp.mocked_session_send)
class TestBlinkFunctions(unittest.TestCase):
"""Test Blink and BlinkCamera functions in blinkpy."""
def setUp(self):
"""Set up Blink module."""
self.blink = blinkpy.Blink(username=USERNAME,
password=PASSWORD)
self.blink = blinkpy.Blink(username=USERNAME, password=PASSWORD)
# pylint: disable=protected-access
self.blink._auth_header = {
'Host': 'test.url.tld',
'TOKEN_AUTH': 'foobar123'
}
self.blink.urls = blinkpy.BlinkURLHandler('test')
self.blink._auth_header = {"Host": "test.url.tld", "TOKEN_AUTH": "foobar123"}
self.blink.urls = blinkpy.BlinkURLHandler("test")
self.blink.session = create_session()
def tearDown(self):
"""Clean up after test."""
self.blink = None
@mock.patch('blinkpy.blinkpy.api.http_req')
@mock.patch("blinkpy.login_handler.api.request_login")
def test_backup_url(self, req, mock_sess):
"""Test backup login method."""
fake_req = Request('POST', 'http://wrong.url').prepare()
req.side_effect = [
mresp.mocked_session_send(fake_req),
{'authtoken': {'authtoken': 'foobar123'},
'networks': {'1234': {'name': 'foobar', 'onboarded': True}}}
]
self.blink.get_auth_token()
self.assertEqual(self.blink.region_id, 'piri')
self.assertEqual(self.blink.region, 'UNKNOWN')
# pylint: disable=protected-access
self.assertEqual(self.blink._token, 'foobar123')
json_resp = {
"authtoken": {"authtoken": "foobar123"},
"networks": {"1234": {"name": "foobar", "onboarded": True}},
}
bad_req = mresp.MockResponse({}, 404)
new_req = mresp.MockResponse(json_resp, 200)
req.side_effect = [bad_req, bad_req, new_req]
self.blink.login_handler.login_urls = ["test1", "test2", "test3"]
self.blink.login_handler.login(self.blink)
self.assertEqual(self.blink.login_handler.login_url, "test3")
req.side_effect = [bad_req, new_req, bad_req]
self.blink.login_handler.login_urls = ["test1", "test2", "test3"]
self.blink.login_handler.login(self.blink)
self.assertEqual(self.blink.login_handler.login_url, "test2")
def test_merge_cameras(self, mock_sess):
"""Test merge camera functionality."""
first_dict = {'foo': 'bar', 'test': 123}
next_dict = {'foobar': 456, 'bar': 'foo'}
self.blink.sync['foo'] = BlinkSyncModule(self.blink, 'foo', 1)
self.blink.sync['bar'] = BlinkSyncModule(self.blink, 'bar', 2)
self.blink.sync['foo'].cameras = first_dict
self.blink.sync['bar'].cameras = next_dict
first_dict = {"foo": "bar", "test": 123}
next_dict = {"foobar": 456, "bar": "foo"}
self.blink.sync["foo"] = BlinkSyncModule(self.blink, "foo", 1, [])
self.blink.sync["bar"] = BlinkSyncModule(self.blink, "bar", 2, [])
self.blink.sync["foo"].cameras = first_dict
self.blink.sync["bar"].cameras = next_dict
result = self.blink.merge_cameras()
expected = {'foo': 'bar', 'test': 123, 'foobar': 456, 'bar': 'foo'}
expected = {"foo": "bar", "test": 123, "foobar": 456, "bar": "foo"}
self.assertEqual(expected, result)
@mock.patch("blinkpy.blinkpy.api.request_videos")
def test_download_video_exit(self, mock_req, mock_sess):
"""Test we exit method when provided bad response."""
blink = blinkpy.Blink()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
blink.last_refresh = 0
mock_req.return_value = {}
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"INFO:blinkpy.blinkpy:No videos found on page 1. Exiting.",
]
with self.assertLogs() as dl_log:
blink.download_videos("/tmp")
self.assertEqual(dl_log.output, expected_log)
@mock.patch("blinkpy.blinkpy.api.request_videos")
def test_parse_downloaded_items(self, mock_req, mock_sess):
"""Test ability to parse downloaded items list."""
blink = blinkpy.Blink()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
generic_entry = {
"created_at": "1970",
"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)
+98 -77
View File
@@ -11,140 +11,161 @@ from unittest import mock
from blinkpy import api
from blinkpy.blinkpy import Blink
from blinkpy.sync_module import BlinkSyncModule
from blinkpy.login_handler import LoginHandler
from blinkpy.helpers.util import (
http_req, create_session, BlinkAuthenticationException,
BlinkException, BlinkURLHandler)
http_req,
create_session,
BlinkException,
BlinkURLHandler,
)
from blinkpy.helpers.constants import __version__
import tests.mock_responses as mresp
USERNAME = 'foobar'
PASSWORD = 'deadbeef'
USERNAME = "foobar"
PASSWORD = "deadbeef"
@mock.patch('blinkpy.helpers.util.Session.send',
side_effect=mresp.mocked_session_send)
@mock.patch("blinkpy.helpers.util.Session.send", side_effect=mresp.mocked_session_send)
class TestBlinkSetup(unittest.TestCase):
"""Test the Blink class in blinkpy."""
def setUp(self):
"""Set up Blink module."""
self.blink_no_cred = Blink()
self.blink = Blink(username=USERNAME,
password=PASSWORD)
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', '1234')
self.blink.urls = BlinkURLHandler('test')
self.blink = Blink(username=USERNAME, password=PASSWORD)
self.blink.sync["test"] = BlinkSyncModule(self.blink, "test", "1234", [])
self.blink.urls = BlinkURLHandler("test")
self.blink.session = create_session()
def tearDown(self):
"""Clean up after test."""
self.blink = None
self.blink_no_cred = None
def test_initialization(self, mock_sess):
"""Verify we can initialize blink."""
# pylint: disable=protected-access
self.assertEqual(self.blink._username, USERNAME)
# pylint: disable=protected-access
self.assertEqual(self.blink._password, PASSWORD)
def test_no_credentials(self, mock_sess):
"""Check that we throw an exception when no username/password."""
with self.assertRaises(BlinkAuthenticationException):
self.blink_no_cred.get_auth_token()
# pylint: disable=protected-access
self.blink_no_cred._username = USERNAME
with self.assertRaises(BlinkAuthenticationException):
self.blink_no_cred.get_auth_token()
def test_no_auth_header(self, mock_sess):
"""Check that we throw an exception when no auth header given."""
# pylint: disable=unused-variable
(region_id, region), = mresp.LOGIN_RESPONSE['region'].items()
self.blink.urls = BlinkURLHandler(region_id)
with self.assertRaises(BlinkException):
self.blink.get_ids()
@mock.patch('blinkpy.blinkpy.getpass.getpass')
def test_manual_login(self, getpwd, mock_sess):
"""Check that we can manually use the login() function."""
getpwd.return_value = PASSWORD
with mock.patch('builtins.input', return_value=USERNAME):
self.assertTrue(self.blink_no_cred.login())
# pylint: disable=protected-access
self.assertEqual(self.blink_no_cred._username, USERNAME)
# pylint: disable=protected-access
self.assertEqual(self.blink_no_cred._password, PASSWORD)
self.assertEqual(self.blink.version, __version__)
self.assertEqual(self.blink.login_handler.data["username"], USERNAME)
self.assertEqual(self.blink.login_handler.data["password"], PASSWORD)
def test_bad_request(self, mock_sess):
"""Check that we raise an Exception with a bad request."""
self.blink.session = create_session()
explog = ("ERROR:blinkpy.helpers.util:"
"Cannot obtain new token for server auth.")
explog = "WARNING:blinkpy.helpers.util:" "Response from server: 200 - foo"
with self.assertRaises(BlinkException):
http_req(self.blink, reqtype='bad')
http_req(self.blink, reqtype="bad")
with self.assertLogs() as logrecord:
http_req(self.blink, reqtype='post', is_retry=True)
http_req(self.blink, reqtype="post", is_retry=True)
self.assertEqual(logrecord.output, [explog])
def test_authentication(self, mock_sess):
"""Check that we can authenticate Blink up properly."""
authtoken = self.blink.get_auth_token()['TOKEN_AUTH']
expected = mresp.LOGIN_RESPONSE['authtoken']['authtoken']
authtoken = self.blink.get_auth_token()["TOKEN_AUTH"]
expected = mresp.LOGIN_RESPONSE["authtoken"]["authtoken"]
self.assertEqual(authtoken, expected)
def test_reauthorization_attempt(self, mock_sess):
"""Check that we can reauthorize after first unsuccessful attempt."""
original_header = self.blink.get_auth_token()
# pylint: disable=protected-access
bad_header = {'Host': self.blink._host, 'TOKEN_AUTH': 'BADTOKEN'}
bad_header = {"Host": self.blink._host, "TOKEN_AUTH": "BADTOKEN"}
# pylint: disable=protected-access
self.blink._auth_header = bad_header
self.assertEqual(self.blink.auth_header, bad_header)
api.request_homescreen(self.blink)
self.assertEqual(self.blink.auth_header, original_header)
@mock.patch('blinkpy.api.request_networks')
def test_multiple_networks(self, mock_net, mock_sess):
def test_multiple_networks(self, mock_sess):
"""Check that we handle multiple networks appropriately."""
mock_net.return_value = {
'networks': [{'id': 1234, 'account_id': 1111},
{'id': 5678, 'account_id': 2222}]
self.blink.networks = {
"0000": {"onboarded": False, "name": "foo"},
"5678": {"onboarded": True, "name": "bar"},
"1234": {"onboarded": False, "name": "test"},
}
self.blink.networks = {'0000': {'onboarded': False, 'name': 'foo'},
'5678': {'onboarded': True, 'name': 'bar'},
'1234': {'onboarded': False, 'name': 'test'}}
self.blink.get_ids()
self.assertTrue('5678' in self.blink.network_ids)
self.assertEqual(self.blink.account_id, 2222)
self.assertTrue("5678" in self.blink.network_ids)
@mock.patch('blinkpy.api.request_networks')
def test_multiple_onboarded_networks(self, mock_net, mock_sess):
def test_multiple_onboarded_networks(self, mock_sess):
"""Check that we handle multiple networks appropriately."""
mock_net.return_value = {
'networks': [{'id': 0000, 'account_id': 2222},
{'id': 5678, 'account_id': 1111}]
self.blink.networks = {
"0000": {"onboarded": False, "name": "foo"},
"5678": {"onboarded": True, "name": "bar"},
"1234": {"onboarded": True, "name": "test"},
}
self.blink.networks = {'0000': {'onboarded': False, 'name': 'foo'},
'5678': {'onboarded': True, 'name': 'bar'},
'1234': {'onboarded': True, 'name': 'test'}}
self.blink.get_ids()
self.assertTrue('5678' in self.blink.network_ids)
self.assertTrue('1234' in self.blink.network_ids)
self.assertEqual(self.blink.account_id, 1111)
self.assertTrue("0000" not in self.blink.network_ids)
self.assertTrue("5678" in self.blink.network_ids)
self.assertTrue("1234" in self.blink.network_ids)
@mock.patch('blinkpy.blinkpy.time.time')
@mock.patch("blinkpy.blinkpy.time.time")
def test_throttle(self, mock_time, mock_sess):
"""Check throttling functionality."""
now = self.blink.refresh_rate + 1
mock_time.return_value = now
self.assertEqual(self.blink.last_refresh, None)
result = self.blink.check_if_ok_to_update()
self.assertEqual(self.blink.check_if_ok_to_update(), True)
self.assertEqual(self.blink.last_refresh, None)
with mock.patch(
"blinkpy.sync_module.BlinkSyncModule.refresh", return_value=True
):
self.blink.refresh()
self.assertEqual(self.blink.last_refresh, now)
self.assertEqual(result, True)
self.assertEqual(self.blink.check_if_ok_to_update(), False)
self.assertEqual(self.blink.last_refresh, now)
def test_sync_case_insensitive_dict(self, mock_sess):
"""Check that we can access sync modules ignoring case."""
self.assertEqual(self.blink.sync['test'].name, 'test')
self.assertEqual(self.blink.sync['TEST'].name, 'test')
self.assertEqual(self.blink.sync["test"].name, "test")
self.assertEqual(self.blink.sync["TEST"].name, "test")
@mock.patch("blinkpy.api.request_login")
def test_unexpected_login(self, mock_login, mock_sess):
"""Check that we appropriately handle unexpected login info."""
mock_login.return_value = None
self.assertFalse(self.blink.get_auth_token())
@mock.patch("blinkpy.api.request_homescreen")
def test_get_cameras(self, mock_home, mock_sess):
"""Check retrieval of camera information."""
mock_home.return_value = {
"cameras": [
{"name": "foo", "network_id": 1234, "id": 5678},
{"name": "bar", "network_id": 1234, "id": 5679},
{"name": "test", "network_id": 4321, "id": 0000},
]
}
result = self.blink.get_cameras()
self.assertEqual(
result,
{
"1234": [{"name": "foo", "id": 5678}, {"name": "bar", "id": 5679}],
"4321": [{"name": "test", "id": 0000}],
},
)
@mock.patch("blinkpy.api.request_homescreen")
def test_get_cameras_failure(self, mock_home, mock_sess):
"""Check that on failure we initialize empty info and move on."""
mock_home.return_value = {}
result = self.blink.get_cameras()
self.assertEqual(result, {})
@mock.patch.object(LoginHandler, "send_auth_key")
@mock.patch.object(Blink, "setup_post_verify")
def test_startup_prompt(self, mock_send_key, mock_verify, mock_sess):
"""Test startup logic with command-line prompt."""
mock_send_key.return_value = True
mock_verify.return_value = True
self.blink.no_prompt = False
self.blink.key_required = True
self.blink.available = True
with mock.patch("builtins.input", return_value="1234"):
self.blink.start()
self.assertFalse(self.blink.key_required)
def test_startup_no_prompt(self, mock_sess):
"""Test startup with no_prompt flag set."""
self.blink.key_required = True
self.blink.no_prompt = True
self.blink.start()
self.assertTrue(self.blink.key_required)
+87 -134
View File
@@ -11,191 +11,144 @@ from unittest import mock
from blinkpy import blinkpy
from blinkpy.helpers.util import create_session, BlinkURLHandler
from blinkpy.sync_module import BlinkSyncModule
from blinkpy.camera import BlinkCamera, MAX_CLIPS
from blinkpy.camera import BlinkCamera
import tests.mock_responses as mresp
USERNAME = 'foobar'
PASSWORD = 'deadbeef'
USERNAME = "foobar"
PASSWORD = "deadbeef"
CAMERA_CFG = {
'camera': [
"camera": [
{
'battery_voltage': 90,
'motion_alert': True,
'wifi_strength': -30,
'temperature': 68
"battery_voltage": 90,
"motion_alert": True,
"wifi_strength": -30,
"temperature": 68,
}
]
}
@mock.patch('blinkpy.helpers.util.Session.send',
side_effect=mresp.mocked_session_send)
@mock.patch("blinkpy.helpers.util.Session.send", side_effect=mresp.mocked_session_send)
class TestBlinkCameraSetup(unittest.TestCase):
"""Test the Blink class in blinkpy."""
def setUp(self):
"""Set up Blink module."""
self.blink = blinkpy.Blink(username=USERNAME,
password=PASSWORD)
self.blink = blinkpy.Blink(username=USERNAME, password=PASSWORD)
header = {
'Host': 'abc.zxc',
'TOKEN_AUTH': mresp.LOGIN_RESPONSE['authtoken']['authtoken']
"Host": "abc.zxc",
"TOKEN_AUTH": mresp.LOGIN_RESPONSE["authtoken"]["authtoken"],
}
# pylint: disable=protected-access
self.blink._auth_header = header
self.blink.session = create_session()
self.blink.urls = BlinkURLHandler('test')
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', 1234)
self.camera = BlinkCamera(self.blink.sync['test'])
self.camera.name = 'foobar'
self.blink.sync['test'].cameras['foobar'] = self.camera
self.blink.urls = BlinkURLHandler("test")
self.blink.sync["test"] = BlinkSyncModule(self.blink, "test", 1234, [])
self.camera = BlinkCamera(self.blink.sync["test"])
self.camera.name = "foobar"
self.blink.sync["test"].cameras["foobar"] = self.camera
def tearDown(self):
"""Clean up after test."""
self.blink = None
def test_check_for_motion(self, mock_sess):
"""Test check for motion function."""
self.assertEqual(self.camera.last_record, [])
self.assertEqual(self.camera.motion_detected, None)
self.camera.sync.record_dates = {'foobar': [1, 3, 2, 4]}
self.camera.check_for_motion()
self.assertEqual(self.camera.last_record, [4])
self.assertEqual(self.camera.motion_detected, False)
self.camera.sync.record_dates = {'foobar': [7, 1, 3, 4]}
self.camera.check_for_motion()
self.assertEqual(self.camera.last_record, [7, 4])
self.assertEqual(self.camera.motion_detected, True)
self.camera.check_for_motion()
self.assertEqual(self.camera.last_record, [7, 4])
self.assertEqual(self.camera.motion_detected, False)
def test_max_motion_clips(self, mock_sess):
"""Test that we only maintain certain number of records."""
for i in range(0, MAX_CLIPS):
self.camera.last_record.append(i)
self.camera.sync.record_dates['foobar'] = [MAX_CLIPS+2]
self.assertEqual(len(self.camera.last_record), MAX_CLIPS)
self.camera.check_for_motion()
self.assertEqual(self.camera.motion_detected, True)
self.assertEqual(len(self.camera.last_record), MAX_CLIPS)
def test_camera_update(self, mock_sess):
"""Test that we can properly update camera properties."""
config = {
'name': 'new',
'camera_id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
'battery_voltage': 90,
'battery_state': 'ok',
'temperature': 68,
'wifi_strength': 4,
'thumbnail': '/thumb',
"name": "new",
"id": 1234,
"network_id": 5678,
"serial": "12345678",
"enabled": False,
"battery_voltage": 90,
"battery_state": "ok",
"temperature": 68,
"wifi_strength": 4,
"thumbnail": "/thumb",
}
self.camera.last_record = ["1"]
self.camera.sync.last_record = {
"new": {"clip": "/test.mp4", "time": "1970-01-01T00:00:00"}
}
self.camera.last_record = ['1']
self.camera.sync.all_clips = {'new': {'1': '/test.mp4'}}
mock_sess.side_effect = [
'test',
'foobar'
mresp.MockResponse({"temp": 71}, 200),
"test",
"foobar",
]
self.camera.update(config)
self.assertEqual(self.camera.name, 'new')
self.assertEqual(self.camera.camera_id, '1234')
self.assertEqual(self.camera.network_id, '5678')
self.assertEqual(self.camera.serial, '12345678')
self.assertEqual(self.camera.name, "new")
self.assertEqual(self.camera.camera_id, "1234")
self.assertEqual(self.camera.network_id, "5678")
self.assertEqual(self.camera.serial, "12345678")
self.assertEqual(self.camera.motion_enabled, False)
self.assertEqual(self.camera.battery, 50)
self.assertEqual(self.camera.battery, "ok")
self.assertEqual(self.camera.temperature, 68)
self.assertEqual(self.camera.temperature_c, 20)
self.assertEqual(self.camera.temperature_calibrated, 71)
self.assertEqual(self.camera.wifi_strength, 4)
self.assertEqual(self.camera.thumbnail,
'https://rest.test.immedia-semi.com/thumb.jpg')
self.assertEqual(self.camera.clip,
'https://rest.test.immedia-semi.com/test.mp4')
self.assertEqual(self.camera.image_from_cache, 'test')
self.assertEqual(self.camera.video_from_cache, 'foobar')
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')
self.assertEqual(
self.camera.thumbnail, "https://rest-test.immedia-semi.com/thumb.jpg"
)
self.assertEqual(
self.camera.clip, "https://rest-test.immedia-semi.com/test.mp4"
)
self.assertEqual(self.camera.image_from_cache, "test")
self.assertEqual(self.camera.video_from_cache, "foobar")
def test_no_thumbnails(self, mock_sess):
"""Tests that thumbnail is 'None' if none found."""
mock_sess.return_value = 'foobar'
self.camera.last_record = ['1']
self.camera.sync.record_dates['new'] = ['1']
self.camera.sync.all_clips = {'new': {'1': '/test.mp4'}}
mock_sess.return_value = "foobar"
self.camera.last_record = ["1"]
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': []
"name": "new",
"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": []}
self.assertEqual(self.camera.temperature_calibrated, None)
with self.assertLogs() as logrecord:
self.camera.update(config)
self.camera.update(config, force=True)
self.assertEqual(self.camera.thumbnail, None)
self.assertEqual(self.camera.last_record, ["1"])
self.assertEqual(self.camera.temperature_calibrated, 68)
self.assertEqual(
logrecord.output,
["ERROR:blinkpy.camera:Could not find thumbnail for camera new"]
[
(
"WARNING:blinkpy.camera:Could not retrieve calibrated "
"temperature."
),
(
"WARNING:blinkpy.camera:Could not find thumbnail for camera new"
"\nNoneType: None"
),
],
)
def test_no_video_clips(self, mock_sess):
"""Tests that we still proceed with camera setup with no videos."""
mock_sess.return_value = 'foobar'
mock_sess.return_value = "foobar"
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': '/foobar',
}
self.camera.sync.homescreen = {
'devices': []
"name": "new",
"id": 1234,
"network_id": 5678,
"serial": "12345678",
"enabled": False,
"battery_voltage": 90,
"battery_state": "ok",
"temperature": 68,
"wifi_strength": 4,
"thumbnail": "/foobar",
}
self.camera.sync.homescreen = {"devices": []}
self.camera.update(config, force_cache=True)
self.assertEqual(self.camera.clip, None)
self.assertEqual(self.camera.video_from_cache, None)
+135
View File
@@ -0,0 +1,135 @@
"""Test login handler."""
import unittest
from unittest import mock
from blinkpy.login_handler import LoginHandler
import tests.mock_responses as mresp
USERNAME = "foobar"
PASSWORD = "deadbeef"
@mock.patch("blinkpy.helpers.util.Session.send", side_effect=mresp.mocked_session_send)
class TestLoginHandler(unittest.TestCase):
"""Test the LoginHandler class in blinkpy."""
def setUp(self):
"""Set up Login Handler."""
self.login_handler = LoginHandler()
def tearDown(self):
"""Clean up after test."""
self.login_handler = None
@mock.patch("blinkpy.login_handler.getpass")
def test_manual_login(self, getpwd, mock_sess):
"""Check that we can manually use the login() function."""
getpwd.return_value = PASSWORD
with mock.patch("builtins.input", return_value=USERNAME):
self.assertTrue(self.login_handler.check_login())
self.assertEqual(self.login_handler.data["username"], USERNAME)
self.assertEqual(self.login_handler.data["password"], PASSWORD)
def test_no_cred_file(self, mock_sess):
"""Check that we return false when cred file doesn't exist."""
self.login_handler.cred_file = "/tmp/fake.file"
self.assertFalse(self.login_handler.check_cred_file())
@mock.patch("blinkpy.login_handler.isfile")
def test_exit_on_missing_json(self, mockisfile, mock_sess):
"""Test that we fail on missing json data."""
self.login_handler.cred_file = "/tmp/fake.file"
mockisfile.return_value = True
with mock.patch("builtins.open", mock.mock_open(read_data="{}")):
self.assertFalse(self.login_handler.check_cred_file())
@mock.patch("blinkpy.login_handler.json.load")
@mock.patch("blinkpy.login_handler.isfile")
def test_cred_file(self, mockisfile, mockjson, mock_sess):
"""Test that loading credential file works."""
self.login_handler.cred_file = "/tmp/fake.file"
mockjson.return_value = {"username": "foo", "password": "bar"}
mockisfile.return_value = True
with mock.patch("builtins.open", mock.mock_open(read_data="")):
self.assertTrue(self.login_handler.check_cred_file())
self.assertEqual(self.login_handler.data["username"], "foo")
self.assertEqual(self.login_handler.data["password"], "bar")
def test_bad_response(self, mock_sess):
"""Check bad response from server."""
self.assertFalse(self.login_handler.validate_response(None, None))
def test_bad_response_code(self, mock_sess):
"""Check bad response code from server."""
fake_resp = mresp.MockResponse(None, 404)
self.assertFalse(self.login_handler.validate_response(None, fake_resp))
def test_good_response_code(self, mock_sess):
"""Check good response code from server."""
fake_resp = mresp.MockResponse(None, 200)
self.assertTrue(self.login_handler.validate_response(None, fake_resp))
@mock.patch("blinkpy.login_handler.util.gen_uid")
def test_check_keys_no_persist(self, mock_uid, mock_sess):
"""Check key generation."""
uid_value = "abc123"
mock_uid.return_value = "abc123"
self.login_handler.persist_key = None
data = self.login_handler.check_keys()
self.assertEqual(data["uid"], uid_value)
self.assertEqual(data["notification_key"], uid_value)
@mock.patch("blinkpy.login_handler.util.gen_uid")
@mock.patch("blinkpy.login_handler.json.load")
@mock.patch("blinkpy.login_handler.isfile")
def test_check_keys_persist(self, mockisfile, mockjson, mock_uid, mock_sess):
"""Check key load from file."""
uid_value = "abc123"
mock_file = {"uid": "321cba", "notification_key": "foobar123"}
mock_uid.return_value = uid_value
mockjson.return_value = mock_file
mockisfile.return_value = True
self.login_handler.persist_key = True
data = self.login_handler.check_keys()
self.assertEqual(mock_file["uid"], data["uid"])
self.assertEqual(mock_file["notification_key"], data["notification_key"])
def test_check_key_required(self, mock_sess):
"""Check key required method."""
response_true = {"client": {"verification_required": True}}
response_false = {"client": {"verification_required": False}}
response_nokey = {}
mock_blink = MockBlink(response_nokey)
self.assertFalse(self.login_handler.check_key_required(mock_blink))
mock_blink = MockBlink(response_false)
self.assertFalse(self.login_handler.check_key_required(mock_blink))
mock_blink = MockBlink(response_true)
self.assertTrue(self.login_handler.check_key_required(mock_blink))
@mock.patch("blinkpy.login_handler.api.request_verify")
def test_send_auth_key(self, mock_req, mock_sess):
"""Check sending of auth key."""
mock_blink = MockBlink(None)
mock_req.return_value = mresp.MockResponse({"valid": True}, 200)
self.assertTrue(self.login_handler.send_auth_key(mock_blink, 1234))
self.assertTrue(mock_blink.available)
mock_req.return_value = mresp.MockResponse(None, 200)
self.assertFalse(self.login_handler.send_auth_key(mock_blink, 1234))
mock_req.return_value = mresp.MockResponse({}, 200)
self.assertFalse(self.login_handler.send_auth_key(mock_blink, 1234))
self.assertTrue(self.login_handler.send_auth_key(mock_blink, None))
class MockBlink:
"""Object to mock basic blink class."""
def __init__(self, login_response):
"""Initialize mock blink class."""
self.available = False
self.login_response = login_response
+193 -67
View File
@@ -6,95 +6,221 @@ from blinkpy import blinkpy
from blinkpy.sync_module import BlinkSyncModule
from blinkpy.camera import BlinkCamera
USERNAME = 'foobar'
PASSWORD = 'deadbeef'
USERNAME = "foobar"
PASSWORD = "deadbeef"
@mock.patch('blinkpy.api.http_req')
@mock.patch("blinkpy.api.http_req")
class TestBlinkSyncModule(unittest.TestCase):
"""Test BlinkSyncModule functions in blinkpy."""
def setUp(self):
"""Set up Blink module."""
self.blink = blinkpy.Blink(username=USERNAME,
password=PASSWORD)
self.blink = blinkpy.Blink(
username=USERNAME, password=PASSWORD, motion_interval=0
)
# pylint: disable=protected-access
self.blink._auth_header = {
'Host': 'test.url.tld',
'TOKEN_AUTH': 'foobar123'
}
self.blink.urls = blinkpy.BlinkURLHandler('test')
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', '1234')
self.blink._auth_header = {"Host": "test.url.tld", "TOKEN_AUTH": "foobar123"}
self.blink.last_refresh = 0
self.blink.urls = blinkpy.BlinkURLHandler("test")
self.blink.sync["test"] = BlinkSyncModule(self.blink, "test", "1234", [])
self.camera = BlinkCamera(self.blink.sync)
self.mock_start = [
{
"syncmodule": {
"id": 1234,
"network_id": 5678,
"serial": "12345678",
"status": "foobar",
}
},
{"event": True},
{},
{},
None,
{"devicestatus": {}},
]
self.blink.sync["test"].network_info = {"network": {"armed": True}}
def tearDown(self):
"""Clean up after test."""
self.blink = None
self.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)
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)
mock_resp.return_value = {"camera": ["foobar"]}
self.assertEqual(self.blink.sync["test"].get_camera_info("1234"), "foobar")
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_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_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_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 = [
{'syncmodule': {
'id': 1234,
'network_id': 5678,
'serial': '12345678',
'status': 'foobar'}},
{'event': True},
{},
{},
{'devicestatus': {}},
None,
None
]
self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].name, 'test')
self.assertEqual(self.blink.sync['test'].sync_id, 1234)
self.assertEqual(self.blink.sync['test'].network_id, 5678)
self.assertEqual(self.blink.sync['test'].serial, '12345678')
self.assertEqual(self.blink.sync['test'].status, 'foobar')
mock_resp.side_effect = self.mock_start
self.blink.sync["test"].start()
self.assertEqual(self.blink.sync["test"].name, "test")
self.assertEqual(self.blink.sync["test"].sync_id, 1234)
self.assertEqual(self.blink.sync["test"].network_id, 5678)
self.assertEqual(self.blink.sync["test"].serial, "12345678")
self.assertEqual(self.blink.sync["test"].status, "foobar")
def test_unexpected_summary(self, mock_resp):
"""Test unexpected summary response."""
self.mock_start[0] = None
mock_resp.side_effect = self.mock_start
self.assertFalse(self.blink.sync["test"].start())
def test_summary_with_no_network_id(self, mock_resp):
"""Test handling of bad summary."""
self.mock_start[0]["syncmodule"] = None
mock_resp.side_effect = self.mock_start
self.assertFalse(self.blink.sync["test"].start())
def test_summary_with_only_network_id(self, mock_resp):
"""Test handling of sparse summary."""
self.mock_start[0]["syncmodule"] = {"network_id": 8675309}
mock_resp.side_effect = self.mock_start
self.blink.sync["test"].start()
self.assertEqual(self.blink.sync["test"].network_id, 8675309)
def test_unexpected_camera_info(self, mock_resp):
"""Test unexpected camera info response."""
self.blink.sync["test"].cameras["foo"] = None
self.mock_start[5] = None
mock_resp.side_effect = self.mock_start
self.blink.sync["test"].start()
self.assertEqual(self.blink.sync["test"].cameras, {"foo": None})
def test_missing_camera_info(self, mock_resp):
"""Test missing key from camera info response."""
self.blink.sync["test"].cameras["foo"] = None
self.mock_start[5] = {}
self.blink.sync["test"].start()
self.assertEqual(self.blink.sync["test"].cameras, {"foo": None})
+116
View File
@@ -0,0 +1,116 @@
"""Test various api functions."""
import unittest
from unittest import mock
import time
from blinkpy.helpers.util import Throttle, BlinkURLHandler, time_to_seconds
class TestUtil(unittest.TestCase):
"""Test the helpers/util module."""
def setUp(self):
"""Initialize the blink module."""
def tearDown(self):
"""Tear down blink module."""
def test_throttle(self):
"""Test the throttle decorator."""
calls = []
@Throttle(seconds=5)
def test_throttle():
calls.append(1)
now = int(time.time())
now_plus_four = now + 4
now_plus_six = now + 6
test_throttle()
self.assertEqual(1, len(calls))
# Call again, still shouldn't fire
test_throttle()
self.assertEqual(1, len(calls))
# Call with force
test_throttle(force=True)
self.assertEqual(2, len(calls))
# Call without throttle, shouldn't fire
test_throttle()
self.assertEqual(2, len(calls))
# Fake time as 4 seconds from now
with mock.patch("time.time", return_value=now_plus_four):
test_throttle()
self.assertEqual(2, len(calls))
# Fake time as 6 seconds from now
with mock.patch("time.time", return_value=now_plus_six):
test_throttle()
self.assertEqual(3, len(calls))
def test_throttle_per_instance(self):
"""Test that throttle is done once per instance of class."""
class Tester:
"""A tester class for throttling."""
def test(self):
"""Test the throttle."""
return True
tester = Tester()
throttled = Throttle(seconds=1)(tester.test)
self.assertEqual(throttled(), True)
self.assertEqual(throttled(), None)
def test_throttle_on_two_methods(self):
"""Test that throttle works for multiple methods."""
class Tester:
"""A tester class for throttling."""
@Throttle(seconds=3)
def test1(self):
"""Test function for throttle."""
return True
@Throttle(seconds=5)
def test2(self):
"""Test function for throttle."""
return True
tester = Tester()
now = time.time()
now_plus_4 = now + 4
now_plus_6 = now + 6
self.assertEqual(tester.test1(), True)
self.assertEqual(tester.test2(), True)
self.assertEqual(tester.test1(), None)
self.assertEqual(tester.test2(), None)
with mock.patch("time.time", return_value=now_plus_4):
self.assertEqual(tester.test1(), True)
self.assertEqual(tester.test2(), None)
with mock.patch("time.time", return_value=now_plus_6):
self.assertEqual(tester.test1(), None)
self.assertEqual(tester.test2(), True)
def test_legacy_subdomains(self):
"""Test that subdomain can be set to legacy mode."""
urls = BlinkURLHandler("test")
self.assertEqual(urls.subdomain, "rest-test")
urls = BlinkURLHandler("test", legacy=True)
self.assertEqual(urls.subdomain, "rest.test")
def test_time_to_seconds(self):
"""Test time to seconds conversion."""
correct_time = "1970-01-01T00:00:05+00:00"
wrong_time = "1/1/1970 00:00:03"
self.assertEqual(time_to_seconds(correct_time), 5)
self.assertFalse(time_to_seconds(wrong_time))
+25 -7
View File
@@ -1,5 +1,5 @@
[tox]
envlist = build, py35, py36, py37, py38, lint
envlist = build, py36, py37, py38, lint
skip_missing_interpreters = True
skipsdist = True
@@ -8,21 +8,39 @@ setenv =
LANG=en_US.UTF-8
PYTHONPATH = {toxinidir}
commands =
py.test --timeout=30 --duration=10 --cov=blinkpy --cov-report term-missing {posargs}
pytest --timeout=9 --durations=10 --cov=blinkpy --cov-report term-missing {posargs}
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements_test.txt
[testenv:lint]
[testenv:cov]
setenv =
LANG=en_US.UTF-8
PYTHONPATH = {toxinidir}
commands =
pytest --timeout=9 --durations=10 --cov=blinkpy --cov-report=xml {posargs}
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements_test.txt
[testenv:pylint]
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements_test.txt
basepython = python3
ignore_errors = True
commands =
pylint --rcfile={toxinidir}/pylintrc blinkpy tests
flake8 blinkpy tests
pydocstyle blinkpy tests
pylint --rcfile={toxinidir}/pylintrc blinkpy tests app
[testenv:lint]
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements_test.txt
basepython = python3
commands =
flake8 blinkpy tests app
pydocstyle blinkpy tests app
black --check --diff blinkpy tests app
rst-lint README.rst
rst-lint CHANGES.rst