Compare commits

...
105 Commits
Author SHA1 Message Date
Kevin FronczakandGitHub 19c9178db7 Merge pull request #223 from fronzbot/fronzbot-patch-1
Update README.rst
2020-04-15 11:51:55 -04:00
Kevin FronczakandGitHub 2ebf2db04d Update README.rst 2020-04-15 11:47:38 -04:00
Kevin FronczakandGitHub 26de89c1b9 Merge pull request #222 from fronzbot/no-camera-enabled-bug
Log response code and message if not trying to re-authorize
2020-04-15 11:26:14 -04:00
Kevin Fronczak 8f68aad238 Create codecov.yml for confugration 2020-04-15 11:21:38 -04:00
Kevin Fronczak 7b586e2947 Log response code and message if not trying to re-authorize (fixes #213) 2020-04-15 11:10:13 -04:00
Kevin FronczakandGitHub 800ddeaf7a Dev version bump 2020-04-15 11:09:46 -04:00
Kevin FronczakandGitHub 4ed2e3dab5 dev version update for pre-release 2020-04-13 20:50:39 -04:00
Kevin FronczakandGitHub 2b9c82b432 Merge pull request #221 from fronzbot/mask-motion-with-arm
Mask motion detection with sync module arm status
2020-04-13 20:48:49 -04:00
Kevin Fronczak 3e7c9f3d5c Mask motion detection with sync module arm status 2020-04-13 20:42:55 -04:00
Kevin FronczakandGitHub 8d0b51ed9c Merge pull request #220 from fronzbot/fix-video-saving
Only store motion clip if motion occurred since last refresh
2020-04-13 20:31:02 -04:00
Kevin Fronczak c178fa5457 Only store motion clip if motion occured since last refresh 2020-04-13 20:02:13 -04:00
Kevin FronczakandGitHub 532ad92714 Merge pull request #219 from fronzbot/json-creds
Load credentials from json file
2020-04-13 19:17:34 -04:00
Kevin Fronczak deb0a682d8 revert tox 2020-04-13 19:05:56 -04:00
Kevin Fronczak 12e22dee8e Load credentials from json file 2020-04-13 18:53:37 -04:00
Kevin FronczakandGitHub ae0f954632 Merge pull request #218 from fronzbot/fix-codecov
Update coverage action
2020-04-13 17:02:40 -04:00
Kevin Fronczak b92941538b Update coverage action 2020-04-13 16:59:53 -04:00
Kevin FronczakandGitHub ce13444427 Merge pull request #217 from fronzbot/bugfix-205
Remove exc_info fixes #205
2020-04-13 16:48:10 -04:00
Kevin FronczakandGitHub 9c461c4cfa Remove exc_info fixes #205 2020-04-13 16:45:36 -04:00
Kevin FronczakandGitHub 23858c2912 Merge pull request #216 from fronzbot/Github-Actions
Migrate to github actions
2020-04-13 16:41:42 -04:00
Kevin FronczakandGitHub 2284a56c82 Delete .travis.yml 2020-04-13 16:37:46 -04:00
Kevin FronczakandGitHub 323143cb2b Fix name 2020-04-13 16:32:50 -04:00
Kevin FronczakandGitHub d6064a9aac Create coverage.yml 2020-04-13 16:32:33 -04:00
Kevin FronczakandGitHub a982aa595f Remove coverage 2020-04-13 16:31:48 -04:00
Kevin FronczakandGitHub 54c52a7f1e Update tests.yml 2020-04-13 16:30:32 -04:00
Kevin FronczakandGitHub bdb8cedf64 Update tests.yml 2020-04-13 16:29:15 -04:00
Kevin FronczakandGitHub 554b18adfe Update README.rst 2020-04-13 16:24:50 -04:00
Kevin FronczakandGitHub 5619af2a00 Update build badge 2020-04-13 16:24:27 -04:00
Kevin FronczakandGitHub 957003aeb1 Rename workflow 2020-04-13 16:22:49 -04:00
Kevin FronczakandGitHub 6fc64a486e Create publish.yml 2020-04-13 16:18:40 -04:00
Kevin FronczakandGitHub 534e3fb34e Reduce to only one VM 2020-04-13 16:14:17 -04:00
Kevin FronczakandGitHub 9ab346ed91 Create tests.yml 2020-04-13 16:11:09 -04:00
Kevin FronczakandGitHub 1bf5dda803 Create lint.yml 2020-04-13 16:02:14 -04:00
Kevin FronczakandGitHub 6cd8fd8ab2 Merge pull request #215 from fronzbot/codecov
Migrate from coveralls to codecov
2020-04-09 15:20:09 -04:00
Kevin Fronczak a689839616 Update codecov badge 2020-04-09 15:16:19 -04:00
Kevin Fronczak 783aa5e547 Migrate from coveralls to codecov 2020-04-09 15:07:30 -04:00
Kevin FronczakandGitHub a6ff2fa0df Merge pull request #214 from fronzbot/bugfix-motion
Fix motion detection getting stuck 'on'
2020-04-09 15:05:44 -04:00
Kevin Fronczak eb498bfb22 Pin coverage in traviswq 2020-04-09 15:01:33 -04:00
Kevin Fronczak 2ebbe46ada Change time conversion to something that supports older python versions 2020-04-09 14:51:06 -04:00
Kevin Fronczak 2f4e8fcade Ignore flake8-black errors 2020-04-09 14:14:47 -04:00
Kevin Fronczak efe05525e4 Fix tests, pin coverage due to sqlite problems 2020-04-09 14:11:19 -04:00
Kevin Fronczak 4122ae8f64 Added time check on latest video 2020-04-08 18:45:59 -04:00
Kevin FronczakandGitHub 5d296cc836 Merge pull request #203 from fronzbot/merge-v0.14.2-into-dev
Merge v0.14.2 into dev
2019-10-12 14:48:45 -04:00
Kevin Fronczak fa50e315a9 dev version bump 2019-10-12 14:42:34 -04:00
Kevin FronczakandGitHub 4a4811bee2 Merge pull request #202 from fronzbot/release-0.14.2
Release 0.14.2
2019-10-12 14:38:35 -04:00
Kevin Fronczak ad7ece4d45 Fixed version.wq 2019-10-12 14:32:26 -04:00
Kevin Fronczak 17d1aa9690 Update changelog, bumped version 2019-10-12 14:31:20 -04:00
Kevin FronczakandGitHub b00839ecb3 Merge pull request #198 from 3ch01c/docker
Add Docker example files
2019-10-12 14:01:25 -04:00
Kevin Fronczak 4900ea8ba6 Add app to tox check 2019-10-12 13:41:57 -04:00
Kevin Fronczak 4fb16398e4 Restructured to allow for future expansion
- Removed docker_compose
2019-10-12 13:27:14 -04:00
Jack Miner a1fc0c5a2b Added TIMEDELTA environment variable to configure how far back to download videos. Wrapped long lines to make linter happy. 2019-09-22 11:43:22 -06:00
Jack Miner 6adb252cdd dockerized 2019-09-14 02:01:44 -06:00
Kevin FronczakandGitHub 3637bbad8d Merge pull request #196 from fronzbot/req
Update python slugify version, better versioning
2019-09-09 11:12:45 -04:00
Kevin FronczakandGitHub 694efe71f8 Update python slugify version, better versioning 2019-09-02 10:29:14 -04:00
Kevin FronczakandGitHub 315735bc2f Merge pull request #192 from fronzbot/dev-version-bump
Dev verison bump
2019-06-20 13:56:16 -04:00
Kevin Fronczak db6403b468 Dev verison bump 2019-06-20 13:44:11 -04:00
Kevin FronczakandGitHub 92dda40e84 Merge pull request #191 from fronzbot/0.14.1
0.14.1
2019-06-20 13:41:22 -04:00
Kevin Fronczak 6c8fc0abce Updated changes 2019-06-20 13:34:59 -04:00
Kevin Fronczak 2d74b12d43 Version bump 2019-06-20 13:32:46 -04:00
Kevin FronczakandGitHub 197695ee40 Merge pull request #190 from fronzbot/dev-version-bump
Dev version bump
2019-06-20 13:24:24 -04:00
Kevin Fronczak 490c42550d Dev version bump
- Added Py3.8 to supported versions
2019-06-20 12:42:01 -04:00
Kevin FronczakandGitHub 7367eb6e87 Merge pull request #187 from fronzbot/upgrade-login
Upgraded login urls to use rest-region subdomain
2019-06-18 10:55:32 -04:00
Kevin FronczakandGitHub 414b647dc8 Merge pull request #189 from fronzbot/timeout
Improved timeout, prevent blocking on startup when unable to login
2019-06-17 11:54:17 -04:00
Kevin FronczakandGitHub 57882bb811 Merge pull request #188 from fronzbot/remove-thumbnail-method
Remove thumbnail from homescreen method
2019-06-17 11:17:15 -04:00
Kevin Fronczak bb76f09f31 Fix dev version (will add to different PR) 2019-06-17 11:15:46 -04:00
Kevin Fronczak 741cf489c8 Fix tests for improved login routine 2019-06-17 11:11:07 -04:00
Kevin Fronczak 7ce332b3ca Improved timeout, prevent blocking on startup when unable to login 2019-06-17 11:07:16 -04:00
Kevin Fronczak 8b7a491c89 Remove thumbnail from homescreen method 2019-06-17 10:20:23 -04:00
Kevin Fronczak 604eebd1b7 Upgraded login urls to use rest-region subdomain 2019-06-17 10:04:34 -04:00
Kevin FronczakandGitHub c2f44b3dfd Merge pull request #183 from fronzbot/dev-version-bump
Dev version bump
2019-05-24 09:38:56 -04:00
Kevin FronczakandGitHub f4480da276 Merge pull request #180 from fronzbot/dev
0.14.0
2019-05-23 16:56:46 -04:00
Kevin Fronczak ccad3dcb0c Dev version bump 2019-05-23 16:53:02 -04:00
Kevin FronczakandGitHub 7767d3af65 Merge pull request #182 from fronzbot/update-changes
Update CHANGES.rst
2019-05-23 16:51:54 -04:00
Kevin FronczakandGitHub 79e699ba78 Update CHANGES.rst 2019-05-23 16:42:12 -04:00
Kevin FronczakandGitHub b28d6f4eb4 Merge pull request #181 from fronzbot/version-bump
Version bump
2019-05-23 16:40:14 -04:00
Kevin FronczakandGitHub d021f48c67 Update CHANGES.rst 2019-05-23 16:15:07 -04:00
Kevin Fronczak 1e71af28dd Version bump 2019-05-23 16:06:56 -04:00
Kevin FronczakandGitHub cb5867d430 Merge pull request #179 from fronzbot/bugfix
Blink instantiation bugfix
2019-05-23 14:45:26 -04:00
Kevin Fronczak bb887936b6 Blink instantiation bugfix 2019-05-23 14:35:27 -04:00
Kevin FronczakandGitHub 720d520398 Merge pull request #175 from fronzbot/update-subdomains
Change subdomain from rest.region to rest-region
2019-05-21 23:56:25 -04:00
Kevin FronczakandGitHub 8fb1116641 Merge branch 'dev' into update-subdomains 2019-05-21 23:37:00 -04:00
Kevin FronczakandGitHub c319762152 Merge pull request #178 from fronzbot/dev-bump
dev version bump
2019-05-21 23:35:25 -04:00
Kevin Fronczak 726d8b8c65 dev version bump 2019-05-21 23:14:26 -04:00
Kevin FronczakandGitHub 683650e2c2 Merge pull request #177 from fronzbot/fix-motion-detect
Fix motion detect
2019-05-21 23:13:15 -04:00
Kevin Fronczak 50b1a35168 Update tests 2019-05-21 22:46:11 -04:00
Kevin Fronczak b88a5feddf Change default interval to one minute 2019-05-21 22:38:49 -04:00
Kevin Fronczak 93472e38de Added in motion interval to allow for looking at historic motion events.
- Good debug utility
- Helps iron out rapid motion events, or missed events due to quick
calls to refresh method
2019-05-21 22:36:46 -04:00
Kevin Fronczak 85c14ede8d Move constants into helpers/constants.py 2019-05-21 22:23:31 -04:00
Kevin Fronczak 58ce109518 Use UTC for time conversions 2019-05-21 22:09:36 -04:00
Kevin Fronczak cf65648293 Add legacy flag to be able to use old 'rest.region' subdomain 2019-05-20 21:15:08 -04:00
Kevin FronczakandGitHub d46e7ed96d Merge pull request #176 from fronzbot/slug
Slugify filenames in video download to ensure OS interoperability
2019-05-20 20:43:28 -04:00
Kevin Fronczak 00395b3825 Slugify filenames in video download to ensure OS interoperability 2019-05-20 11:04:58 -04:00
Kevin FronczakandGitHub f67f0dd6a2 Dev version bump 2019-05-20 00:00:57 -04:00
Kevin FronczakandGitHub d90cd3309f Update README.rst 2019-05-19 18:00:42 -04:00
Kevin Fronczak 130aa8382a Change subdomain from rest.region to rest-region 2019-05-18 18:23:46 -04:00
Kevin FronczakandGitHub e836a6fa58 Merge pull request #174 from fronzbot/fix-battery-voltage
Change battery percentage to state
2019-05-18 18:17:38 -04:00
Kevin Fronczak 92d89b9fe5 Change battery percentage to state 2019-05-18 12:42:01 -04:00
Kevin FronczakandGitHub c10fb432f7 Merge pull request #173 from fronzbot/reduce-throttling
Remove throttling from critical api methods
2019-05-18 12:23:25 -04:00
Kevin Fronczak 37b729b597 Remove throttling from critical api methods 2019-05-18 12:15:57 -04:00
Kevin FronczakandGitHub 749c68bb8a Merge pull request #172 from fronzbot/fix-video-api
Changed log to print
2019-05-18 11:57:46 -04:00
Kevin Fronczak dcf0ce6394 Fix lint issue 2019-05-18 11:51:58 -04:00
Kevin Fronczak 102190e61b Changed log to print 2019-05-18 11:41:17 -04:00
Kevin FronczakandGitHub a7340c97ca Merge pull request #171 from fronzbot/fix-video-api
Added changed video download endpoint
2019-05-18 11:40:23 -04:00
Kevin Fronczak 43c1162634 Added vieo endpoint key changes to motion detect logic 2019-05-18 10:54:24 -04:00
Kevin Fronczak 4db7a33ef3 Added changed video download endpoint 2019-05-18 10:48:58 -04:00
Kevin FronczakandGitHub bfdc1e47bd Dev version bump 2019-03-01 22:00:37 -05:00
32 changed files with 645 additions and 195 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
[run] [run]
omit = parallel = true
helpers/* omit =
tests/* tests/*
setup.py setup.py
+2
View File
@@ -0,0 +1,2 @@
[flake8]
ignore = BLK100
+41
View File
@@ -0,0 +1,41 @@
name: coverage
on:
push:
branches: [ master, dev ]
pull_request:
branches: [ master, dev ]
jobs:
build:
runs-on: ${{ matrix.platform }}
strategy:
max-parallel: 4
matrix:
platform:
- ubuntu-latest
python-version: [3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements_test.txt
pip install tox
pip install codecov
- name: Test
run: |
tox -r -e cov
- name: Codecov
uses: codecov/codecov-action@v1.0.6
with:
flags: unittests
file: ./coverage.xml
name: blinkpy
fail_ci_if_error: true
+34
View File
@@ -0,0 +1,34 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Lint
on:
push:
branches: [ master, dev ]
pull_request:
branches: [ master, dev ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements_test.txt
pip install tox
- name: Lint
run: |
tox -r -e lint
+31
View File
@@ -0,0 +1,31 @@
# This workflows will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
name: Upload Python Package
on:
release:
types: [created]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: '3.7'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py bdist_wheel
twine upload dist/*
+33
View File
@@ -0,0 +1,33 @@
name: build
on:
push:
branches: [ master, dev ]
pull_request:
branches: [ master, dev ]
jobs:
build:
runs-on: ${{ matrix.platform }}
strategy:
max-parallel: 4
matrix:
platform:
- ubuntu-latest
python-version: [3.5, 3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements_test.txt
pip install tox
- name: Test
run: |
tox -r
+1
View File
@@ -4,6 +4,7 @@
__pycache__/* __pycache__/*
htmlcov/* htmlcov/*
.coverage .coverage
coverage.xml
*.pyc *.pyc
*.egg*/* *.egg*/*
dist/* dist/*
+4
View File
@@ -4,4 +4,8 @@ python:
ignored_directories: ignored_directories:
- docs - docs
flake8:
enabled: true
config_file: .flake8
fail_on_violations: true fail_on_violations: true
-23
View File
@@ -1,23 +0,0 @@
sudo: required
matrix:
fast_finish: true
include:
- python: "3.5.3"
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
+38 -5
View File
@@ -3,6 +3,39 @@ Changelog
A list of changes between each release A list of changes between each release
0.14.2 (2019-10-12)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Update dependencies
- Dockerize `(@3ch01c <https://github.com/fronzbot/blinkpy/pull/198>__)`
0.14.1 (2019-06-20)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Fix timeout problems blocking blinkpy startup
- Updated login urls using ``rest-region`` subdomain
- Removed deprecated thumbanil recovery from homescreen
0.14.0 (2019-05-23)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**Breaking Changes:**
- ``BlinkCamera.battery`` no longer reports a percentage, instead it returns a string representing the state of the battery.
- Previous logic for calculating percentage was incorrect
- raw battery voltage can be accessed via ``BlinkCamera.battery_voltage``
**Bug Fixes:**
- Updated video endpoint (fixes broken motion detection)
- Removed throttling from critical api methods which prevented proper operation of multi-sync unit setups
- Slugify downloaded video names to allow for OS interoperability
- Added one minute offset (``Blink.motion_interval``) when checking for recent motion to allow time for events to propagate to server prior to refresh call.
**Everything else:**
- Changed all urls to use ``rest-region`` rather than ``rest.region``. Ability to revert to old method is enabled by instantiating ``Blink()`` with the ``legacy_subdomain`` variable set to ``True``.
- Added debug mode to ``blinkpy.download_videos`` routine to simply print the videos prepped for download, rather than actually saving them.
- Use UTC for time conversions, rather than local timezone
0.13.1 (2019-03-01) 0.13.1 (2019-03-01)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Remove throttle decorator from network status request - Remove throttle decorator from network status request
@@ -10,10 +43,10 @@ A list of changes between each release
0.13.0 (2019-03-01) 0.13.0 (2019-03-01)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**Breaking change:** **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. 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 - Adds throttle decorator
- Decorate following functions with 4s throttle (call method with `force=True` to override): - Decorate following functions with 4s throttle (call method with ``force=True`` to override):
- request_network_status - request_network_status
- request_syncmodule - request_syncmodule
- request_system_arm - request_system_arm
@@ -28,7 +61,7 @@ Wifi status reported in dBm again, instead of bars (which is great). Also, the
- request_motion_detection_enable - request_motion_detection_enable
- request_motion_detection_disable - 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. - 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. - 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 - Add ability to cycle through login api endpoints to anticipate future endpoint deprecation
@@ -70,7 +103,7 @@ Wifi status reported in dBm again, instead of bars (which is great). Also, the
0.10.2 (2018-10-30) 0.10.2 (2018-10-30)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Set minimum required version of the requests library to 2.20.0 due to vulnerability in earlier releases. - Set minimum required version of the requests library to 2.20.0 due to vulnerability in earlier releases.
- When multiple networks detected, changed log level to 'warning' from 'error' - When multiple networks detected, changed log level to ``warning`` from ``error``
0.10.1 (2018-10-18) 0.10.1 (2018-10-18)
@@ -98,7 +131,7 @@ Wifi status reported in dBm again, instead of bars (which is great). Also, the
- Use session for http requests - Use session for http requests
**Breaking change:** **Breaking change:**
- Cameras now accessed through sync module Blink.sync.cameras - Cameras now accessed through sync module ``Blink.sync.cameras``
0.8.1 (2018-09-24) 0.8.1 (2018-09-24)
+12
View File
@@ -0,0 +1,12 @@
FROM python:3.7-alpine
LABEL maintainer="Kevin Fronczak <kfronczak@gmail.com>"
VOLUME /media
RUN python -m pip install --upgrade pip
RUN pip3 install blinkpy
COPY app/ .
ENTRYPOINT ["python", "./app.py"]
CMD []
+28 -4
View File
@@ -2,6 +2,10 @@ blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version| |Python Version|
================================================================================ ================================================================================
A Python library for the Blink Camera system A Python library for the Blink Camera system
Like the library? Consider buying me a cup of coffee!
|Donate|
Disclaimer: Disclaimer:
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
Published under the MIT license - See LICENSE file for more details. Published under the MIT license - See LICENSE file for more details.
@@ -51,6 +55,23 @@ The simplest way to use this package from a terminal is to call ``Blink.start()`
If you would like to log in without setting up the cameras or system, you can simply call the ``Blink.login()`` function which will prompt for a username and password and then authenticate with the server. This is useful if you want to avoid use of the ``start()`` function which simply acts as a wrapper for more targeted API methods. If you would like to log in without setting up the cameras or system, you can simply call the ``Blink.login()`` function which will prompt for a username and password and then authenticate with the server. This is useful if you want to avoid use of the ``start()`` function which simply acts as a wrapper for more targeted API methods.
In addition, you can also save your credentials in a json file and initialize Blink with the credential file as follows:
.. code:: python
from blinkpy import blinkpy
blink = blinkpy.Blink(cred_file="path/to/credentials.json")
blink.start()
The credential file must be json formatted with a ``username`` and ``password`` key like follows:
.. code:: json
{
"username": "YOUR USER NAME",
"password": "YOUR PASSWORD"
}
Cameras are instantiated as individual ``BlinkCamera`` classes within a ``BlinkSyncModule`` instance. All of your sync modules are stored within the ``Blink.sync`` dictionary and can be accessed using the name of the sync module as the key (this is the name of your sync module in the Blink App). Cameras are instantiated as individual ``BlinkCamera`` classes within a ``BlinkSyncModule`` instance. All of your sync modules are stored within the ``Blink.sync`` dictionary and can be accessed using the name of the sync module as the key (this is the name of your sync module in the Blink App).
The below code will display cameras and their available attributes: The below code will display cameras and their available attributes:
@@ -96,13 +117,16 @@ Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34a
blink.download_videos('/home/blink', since='2018/07/04 09:34') blink.download_videos('/home/blink', since='2018/07/04 09:34')
.. |Build Status| image:: https://travis-ci.org/fronzbot/blinkpy.svg?branch=dev .. |Build Status| image:: https://github.com/fronzbot/blinkpy/workflows/build/badge.svg
:target: https://travis-ci.org/fronzbot/blinkpy :target: https://github.com/fronzbot/blinkpy/actions?query=workflow%3Abuild
.. |Coverage Status| image:: https://coveralls.io/repos/github/fronzbot/blinkpy/badge.svg?branch=dev .. |Coverage Status| image:: https://codecov.io/gh/fronzbot/blinkpy/branch/dev/graph/badge.svg
:target: https://coveralls.io/github/fronzbot/blinkpy?branch=dev :target: https://codecov.io/gh/fronzbot/blinkpy
.. |PyPi Version| image:: https://img.shields.io/pypi/v/blinkpy.svg .. |PyPi Version| image:: https://img.shields.io/pypi/v/blinkpy.svg
:target: https://pypi.python.org/pypi/blinkpy :target: https://pypi.python.org/pypi/blinkpy
.. |Docs| image:: https://readthedocs.org/projects/blinkpy/badge/?version=latest .. |Docs| image:: https://readthedocs.org/projects/blinkpy/badge/?version=latest
:target: http://blinkpy.readthedocs.io/en/latest/?badge=latest :target: http://blinkpy.readthedocs.io/en/latest/?badge=latest
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/blinkpy.svg .. |Python Version| image:: https://img.shields.io/pypi/pyversions/blinkpy.svg
:target: https://img.shields.io/pypi/pyversions/blinkpy.svg :target: https://img.shields.io/pypi/pyversions/blinkpy.svg
.. |Donate| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif
:target: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UR6Z2B8GXYUCC
+1
View File
@@ -0,0 +1 @@
"""Python init file for app.py."""
+31
View File
@@ -0,0 +1,31 @@
"""Script to run blinkpy as an app."""
from os import environ
from datetime import datetime, timedelta
from blinkpy import blinkpy
USERNAME = environ.get('USERNAME')
PASSWORD = environ.get('PASSWORD')
TIMEDELTA = timedelta(environ.get('TIMEDELTA', 1))
def get_date():
"""Return now - timedelta for blinkpy."""
return (datetime.now() - TIMEDELTA).isoformat()
def download_videos(blink, save_dir='/media'):
"""Make request to download videos."""
blink.download_videos(save_dir, since=get_date())
def start():
"""Startup blink app."""
blink = blinkpy.Blink(username=USERNAME, password=PASSWORD)
blink.start()
return blink
if __name__ == '__main__':
BLINK = start()
download_videos(BLINK)
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
docker build -t fronzbot/blinkpy:latest ./
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# bash run.sh [username] [password]
if [ "$#" -ne 2 ]; then
echo ""
echo "ERROR: Requries Blink username and password as arguments."
echo "bash run.sh [username] [password]"
echo ""
exit 1
fi
set -ex
USER=fronzbot
IMAGE=blinkpy
CONFIG=$HOME/blinkpy_media
USERNAME=$1
PASSWORD=$2
mkdir -p $CONFIG
result=$(docker images -q $IMAGE)
if [ $result ]; then
docker rm $IMAGE
fi
docker run -it --name ${IMAGE} \
-v $CONFIG:/media \
-e USERNAME=${USERNAME} \
-e PASSWORD=${PASSWORD} \
$USER/$IMAGE \
/bin/bash
+2 -5
View File
@@ -40,7 +40,6 @@ def request_networks(blink):
return http_get(blink, url) return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_network_status(blink, network): def request_network_status(blink, network):
""" """
Request network information. Request network information.
@@ -52,7 +51,6 @@ def request_network_status(blink, network):
return http_get(blink, url) return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_syncmodule(blink, network): def request_syncmodule(blink, network):
""" """
Request sync module info. Request sync module info.
@@ -168,12 +166,11 @@ def request_videos(blink, time=None, page=0):
:param page: Page number to get videos from. :param page: Page number to get videos from.
""" """
timestamp = get_time(time) timestamp = get_time(time)
url = "{}/api/v2/videos/changed?since={}&page={}".format( url = "{}/api/v1/accounts/{}/media/changed?since={}&page={}".format(
blink.urls.base_url, timestamp, page) blink.urls.base_url, blink.account_id, timestamp, page)
return http_get(blink, url) return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_cameras(blink, network): def request_cameras(blink, network):
""" """
Request all camera information. Request all camera information.
+73 -32
View File
@@ -17,10 +17,12 @@ import os.path
import time import time
import getpass import getpass
import logging import logging
import json
from shutil import copyfileobj from shutil import copyfileobj
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
from dateutil.parser import parse from dateutil.parser import parse
from slugify import slugify
from blinkpy import api from blinkpy import api
from blinkpy.sync_module import BlinkSyncModule from blinkpy.sync_module import BlinkSyncModule
@@ -29,14 +31,10 @@ from blinkpy.helpers.util import (
create_session, merge_dicts, get_time, BlinkURLHandler, create_session, merge_dicts, get_time, BlinkURLHandler,
BlinkAuthenticationException, Throttle) BlinkAuthenticationException, Throttle)
from blinkpy.helpers.constants import ( from blinkpy.helpers.constants import (
BLINK_URL, LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL) BLINK_URL, LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL,
DEFAULT_MOTION_INTERVAL, DEFAULT_REFRESH, MIN_THROTTLE_TIME)
from blinkpy.helpers.constants import __version__ from blinkpy.helpers.constants import __version__
REFRESH_RATE = 30
# Prevents rapid calls to blink.refresh()
# with the force_cache flag set to True
MIN_THROTTLE_TIME = 2
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -45,17 +43,32 @@ class Blink():
"""Class to initialize communication.""" """Class to initialize communication."""
def __init__(self, username=None, password=None, def __init__(self, username=None, password=None,
refresh_rate=REFRESH_RATE): cred_file=None,
refresh_rate=DEFAULT_REFRESH,
motion_interval=DEFAULT_MOTION_INTERVAL,
legacy_subdomain=False):
""" """
Initialize Blink system. Initialize Blink system.
:param username: Blink username (usually email address) :param username: Blink username (usually email address)
:param password: Blink password :param password: Blink password
:param cred_file: JSON formatted file to store credentials.
If username and password are given, file
is ignored. Otherwise, username and password
are loaded from file.
:param refresh_rate: Refresh rate of blink information. :param refresh_rate: Refresh rate of blink information.
Defaults to 15 (seconds) Defaults to 15 (seconds)
:param motion_interval: How far back to register motion in minutes.
Defaults to last refresh time.
Useful for preventing motion_detected property
from de-asserting too quickly.
:param legacy_subdomain: Set to TRUE to use old 'rest.region'
endpoints (only use if you are having
api issues).
""" """
self._username = username self._username = username
self._password = password self._password = password
self._cred_file = cred_file
self._token = None self._token = None
self._auth_header = None self._auth_header = None
self._host = None self._host = None
@@ -72,7 +85,10 @@ class Blink():
self.cameras = CaseInsensitiveDict({}) self.cameras = CaseInsensitiveDict({})
self.video_list = CaseInsensitiveDict({}) self.video_list = CaseInsensitiveDict({})
self._login_url = LOGIN_URL self._login_url = LOGIN_URL
self.login_urls = []
self.motion_interval = motion_interval
self.version = __version__ self.version = __version__
self.legacy = legacy_subdomain
@property @property
def auth_header(self): def auth_header(self):
@@ -108,8 +124,25 @@ class Blink():
def login(self): def login(self):
"""Prompt user for username and password.""" """Prompt user for username and password."""
self._username = input("Username:") if self._cred_file is not None and os.path.isfile(self._cred_file):
self._password = getpass.getpass("Password:") try:
with open(self._cred_file, 'r') as json_file:
creds = json.load(json_file)
self._username = creds['username']
self._password = creds['password']
except ValueError:
_LOGGER.error("Improperly formated json file %s.",
self._cred_file,
exc_info=True)
return False
except KeyError:
_LOGGER.error("JSON file information incomplete %s.",
exc_info=True)
return False
else:
self._username = input("Username:")
self._password = getpass.getpass("Password:")
if self.get_auth_token(): if self.get_auth_token():
_LOGGER.debug("Login successful!") _LOGGER.debug("Login successful!")
return True return True
@@ -123,9 +156,9 @@ class Blink():
if not isinstance(self._password, str): if not isinstance(self._password, str):
raise BlinkAuthenticationException(ERROR.PASSWORD) raise BlinkAuthenticationException(ERROR.PASSWORD)
login_urls = [LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL] self.login_urls = [LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL]
response = self.login_request(login_urls, is_retry=is_retry) response = self.login_request(is_retry=is_retry)
if not response: if not response:
return False return False
@@ -136,14 +169,14 @@ class Blink():
self._auth_header = {'Host': self._host, self._auth_header = {'Host': self._host,
'TOKEN_AUTH': self._token} 'TOKEN_AUTH': self._token}
self.urls = BlinkURLHandler(self.region_id) self.urls = BlinkURLHandler(self.region_id, legacy=self.legacy)
return self._auth_header return self._auth_header
def login_request(self, login_urls, is_retry=False): def login_request(self, is_retry=False):
"""Make a login request.""" """Make a login request."""
try: try:
login_url = login_urls.pop(0) login_url = self.login_urls.pop(0)
except IndexError: except IndexError:
_LOGGER.error("Could not login to blink servers.") _LOGGER.error("Could not login to blink servers.")
return False return False
@@ -157,14 +190,13 @@ class Blink():
is_retry=is_retry) is_retry=is_retry)
try: try:
if response.status_code != 200: if response.status_code != 200:
response = self.login_request(login_urls) response = self.login_request(is_retry=True)
response = response.json() response = response.json()
(self.region_id, self.region), = response['region'].items() (self.region_id, self.region), = response['region'].items()
except AttributeError: except AttributeError:
_LOGGER.error("Login API endpoint failed with response %s", _LOGGER.error("Login API endpoint failed with response %s",
response, response)
exc_info=True)
return False return False
except KeyError: except KeyError:
@@ -247,7 +279,8 @@ class Blink():
combined = merge_dicts(combined, self.sync[sync].cameras) combined = merge_dicts(combined, self.sync[sync].cameras)
return combined return combined
def download_videos(self, path, since=None, camera='all', stop=10): def download_videos(self, path, since=None,
camera='all', stop=10, debug=False):
""" """
Download all videos from server since specified time. Download all videos from server since specified time.
@@ -258,6 +291,8 @@ class Blink():
:param camera: Camera name to retrieve. Defaults to "all". :param camera: Camera name to retrieve. Defaults to "all".
Use a list for multiple cameras. Use a list for multiple cameras.
:param stop: Page to stop on (~25 items per page. Default page 10). :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: if since is None:
since_epochs = self.last_refresh since_epochs = self.last_refresh
@@ -275,23 +310,23 @@ class Blink():
response = api.request_videos(self, time=since_epochs, page=page) response = api.request_videos(self, time=since_epochs, page=page)
_LOGGER.debug("Processing page %s", page) _LOGGER.debug("Processing page %s", page)
try: try:
result = response['videos'] result = response['media']
if not result: if not result:
raise IndexError raise IndexError
except (KeyError, IndexError): except (KeyError, IndexError):
_LOGGER.info("No videos found on page %s. Exiting.", page) _LOGGER.info("No videos found on page %s. Exiting.", page)
break break
self._parse_downloaded_items(result, camera, path) self._parse_downloaded_items(result, camera, path, debug)
def _parse_downloaded_items(self, result, camera, path): def _parse_downloaded_items(self, result, camera, path, debug):
"""Parse downloaded videos.""" """Parse downloaded videos."""
for item in result: for item in result:
try: try:
created_at = item['created_at'] created_at = item['created_at']
camera_name = item['camera_name'] camera_name = item['device_name']
is_deleted = item['deleted'] is_deleted = item['deleted']
address = item['address'] address = item['media']
except KeyError: except KeyError:
_LOGGER.info("Missing clip information, skipping...") _LOGGER.info("Missing clip information, skipping...")
continue continue
@@ -307,16 +342,22 @@ class Blink():
continue continue
clip_address = "{}{}".format(self.urls.base_url, address) clip_address = "{}{}".format(self.urls.base_url, address)
filename = "{}_{}.mp4".format(camera_name, created_at) filename = "{}-{}".format(camera_name, created_at)
filename = "{}.mp4".format(slugify(filename))
filename = os.path.join(path, filename) filename = os.path.join(path, filename)
if os.path.isfile(filename): if not debug:
_LOGGER.info("%s already exists, skipping...", filename) if os.path.isfile(filename):
continue _LOGGER.info("%s already exists, skipping...", filename)
continue
response = api.http_get(self, url=clip_address, response = api.http_get(self, url=clip_address,
stream=True, json=False) stream=True, json=False)
with open(filename, 'wb') as vidfile: with open(filename, 'wb') as vidfile:
copyfileobj(response.raw, vidfile) copyfileobj(response.raw, vidfile)
_LOGGER.info("Downloaded video to %s", filename) _LOGGER.info("Downloaded video to %s", filename)
else:
print(("Camera: {}, Timestamp: {}, "
"Address: {}, Filename: {}").format(
camera_name, created_at, address, filename))
+10 -24
View File
@@ -41,6 +41,7 @@ class BlinkCamera():
'temperature_c': self.temperature_c, 'temperature_c': self.temperature_c,
'temperature_calibrated': self.temperature_calibrated, 'temperature_calibrated': self.temperature_calibrated,
'battery': self.battery, 'battery': self.battery,
'battery_voltage': self.battery_voltage,
'thumbnail': self.thumbnail, 'thumbnail': self.thumbnail,
'video': self.clip, 'video': self.clip,
'motion_enabled': self.motion_enabled, 'motion_enabled': self.motion_enabled,
@@ -54,8 +55,8 @@ class BlinkCamera():
@property @property
def battery(self): def battery(self):
"""Return battery level as percentage.""" """Return battery as string."""
return round(self.battery_voltage / 180 * 100) return self.battery_state
@property @property
def temperature_c(self): def temperature_c(self):
@@ -116,13 +117,16 @@ class BlinkCamera():
_LOGGER.warning("Could not retrieve calibrated temperature.") _LOGGER.warning("Could not retrieve calibrated temperature.")
# Check if thumbnail exists in config, if not try to # Check if thumbnail exists in config, if not try to
# get it from the homescreen info in teh sync module # get it from the homescreen info in the sync module
# otherwise set it to None and log an error # otherwise set it to None and log an error
new_thumbnail = None new_thumbnail = None
thumb_addr = None
if config['thumbnail']: if config['thumbnail']:
thumb_addr = config['thumbnail'] thumb_addr = config['thumbnail']
else: else:
thumb_addr = self.get_thumb_from_homescreen() _LOGGER.warning("Could not find thumbnail for camera %s",
self.name,
exc_info=True)
if thumb_addr is not None: if thumb_addr is not None:
new_thumbnail = "{}{}.jpg".format(self.sync.urls.base_url, new_thumbnail = "{}{}.jpg".format(self.sync.urls.base_url,
@@ -174,8 +178,7 @@ class BlinkCamera():
copyfileobj(response.raw, imgfile) copyfileobj(response.raw, imgfile)
else: else:
_LOGGER.error("Cannot write image to file, response %s", _LOGGER.error("Cannot write image to file, response %s",
response.status_code, response.status_code)
exc_info=True)
def video_to_file(self, path): def video_to_file(self, path):
"""Write video to file. """Write video to file.
@@ -186,24 +189,7 @@ class BlinkCamera():
response = self._cached_video response = self._cached_video
if response is None: if response is None:
_LOGGER.error("No saved video exist for %s.", _LOGGER.error("No saved video exist for %s.",
self.name, self.name)
exc_info=True)
return return
with open(path, 'wb') as vidfile: with open(path, 'wb') as vidfile:
copyfileobj(response.raw, vidfile) copyfileobj(response.raw, vidfile)
def get_thumb_from_homescreen(self):
"""Retrieve thumbnail from homescreen."""
for device in self.sync.homescreen['devices']:
try:
device_type = device['device_type']
device_name = device['name']
device_thumb = device['thumbnail']
if device_type == 'camera' and device_name == self.name:
return device_thumb
except KeyError:
pass
_LOGGER.error("Could not find thumbnail for camera %s",
self.name,
exc_info=True)
return None
+10 -5
View File
@@ -3,8 +3,8 @@
import os import os
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 13 MINOR_VERSION = 14
PATCH_VERSION = 1 PATCH_VERSION = '3.dev1'
__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION) __version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
@@ -33,6 +33,7 @@ PROJECT_CLASSIFIERS = [
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Topic :: Home Automation' 'Topic :: Home Automation'
] ]
@@ -45,11 +46,11 @@ PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME)
URLS URLS
''' '''
BLINK_URL = 'immedia-semi.com' BLINK_URL = 'immedia-semi.com'
DEFAULT_URL = "{}.{}".format('prod', BLINK_URL) DEFAULT_URL = "{}.{}".format('rest-prod', BLINK_URL)
BASE_URL = "https://{}".format(DEFAULT_URL) BASE_URL = "https://{}".format(DEFAULT_URL)
LOGIN_URL = "{}/api/v2/login".format(BASE_URL) LOGIN_URL = "{}/api/v2/login".format(BASE_URL)
OLD_LOGIN_URL = "{}/login".format(BASE_URL) OLD_LOGIN_URL = "{}/login".format(BASE_URL)
LOGIN_BACKUP_URL = "https://{}.{}/login".format('rest.piri', BLINK_URL) LOGIN_BACKUP_URL = "https://{}.{}/login".format('rest-piri', BLINK_URL)
''' '''
Dictionaries Dictionaries
@@ -59,4 +60,8 @@ ONLINE = {'online': True, 'offline': False}
''' '''
OTHER OTHER
''' '''
TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S%Z' TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S%z'
DEFAULT_MOTION_INTERVAL = 1
DEFAULT_REFRESH = 30
MIN_THROTTLE_TIME = 2
+2
View File
@@ -11,3 +11,5 @@ AUTH_TOKEN = (
"Authentication header incorrect. Are you sure you received your token?" "Authentication header incorrect. Are you sure you received your token?"
) )
REQUEST = (4, "Cannot perform request (get/post type incorrect)") REQUEST = (4, "Cannot perform request (get/post type incorrect)")
BLINK_ERRORS = [101, 400, 404]
+35 -8
View File
@@ -2,8 +2,10 @@
import logging import logging
import time import time
from functools import wraps from calendar import timegm
from functools import partial, wraps
from requests import Request, Session, exceptions from requests import Request, Session, exceptions
import dateutil.parser
from blinkpy.helpers.constants import BLINK_URL, TIMESTAMP_FORMAT from blinkpy.helpers.constants import BLINK_URL, TIMESTAMP_FORMAT
import blinkpy.helpers.errors as ERROR import blinkpy.helpers.errors as ERROR
@@ -11,11 +13,22 @@ import blinkpy.helpers.errors as ERROR
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def time_to_seconds(timestamp):
"""Convert TIMESTAMP_FORMAT time to seconds."""
try:
dtime = dateutil.parser.isoparse(timestamp)
except ValueError:
_LOGGER.error("Incorrect timestamp format for conversion: %s.",
timestamp)
return False
return timegm(dtime.timetuple())
def get_time(time_to_convert=None): def get_time(time_to_convert=None):
"""Create blink-compatible timestamp.""" """Create blink-compatible timestamp."""
if time_to_convert is None: if time_to_convert is None:
time_to_convert = time.time() time_to_convert = time.time()
return time.strftime(TIMESTAMP_FORMAT, time.localtime(time_to_convert)) return time.strftime(TIMESTAMP_FORMAT, time.gmtime(time_to_convert))
def merge_dicts(dict_a, dict_b): def merge_dicts(dict_a, dict_b):
@@ -28,8 +41,14 @@ def merge_dicts(dict_a, dict_b):
def create_session(): def create_session():
"""Create a session for blink communication.""" """
Create a session for blink communication.
From @ericfrederich via
https://github.com/kennethreitz/requests/issues/2011
"""
sess = Session() sess = Session()
sess.get = partial(sess.get, timeout=5)
return sess return sess
@@ -65,18 +84,23 @@ def http_req(blink, url='http://example.com', data=None, headers=None,
prepped = req.prepare() prepped = req.prepare()
try: try:
response = blink.session.send(prepped, stream=stream, timeout=10) response = blink.session.send(prepped, stream=stream)
if json_resp and 'code' in response.json(): if json_resp and 'code' in response.json():
if is_retry: resp_dict = response.json()
code = resp_dict['code']
message = resp_dict['message']
if is_retry and code in ERROR.BLINK_ERRORS:
_LOGGER.error("Cannot obtain new token for server auth.") _LOGGER.error("Cannot obtain new token for server auth.")
return None return None
else: elif code in ERROR.BLINK_ERRORS:
headers = attempt_reauthorization(blink) headers = attempt_reauthorization(blink)
if not headers: if not headers:
raise exceptions.ConnectionError raise exceptions.ConnectionError
return http_req(blink, url=url, data=data, headers=headers, return http_req(blink, url=url, data=data, headers=headers,
reqtype=reqtype, stream=stream, reqtype=reqtype, stream=stream,
json_resp=json_resp, is_retry=True) json_resp=json_resp, is_retry=True)
_LOGGER.warning("Response from server: %s - %s", code, message)
except (exceptions.ConnectionError, exceptions.Timeout): except (exceptions.ConnectionError, exceptions.Timeout):
_LOGGER.info("Cannot connect to server with url %s.", url) _LOGGER.info("Cannot connect to server with url %s.", url)
if not is_retry: if not is_retry:
@@ -111,9 +135,12 @@ class BlinkAuthenticationException(BlinkException):
class BlinkURLHandler(): class BlinkURLHandler():
"""Class that handles Blink URLS.""" """Class that handles Blink URLS."""
def __init__(self, region_id): def __init__(self, region_id, legacy=False):
"""Initialize the urls.""" """Initialize the urls."""
self.base_url = "https://rest.{}.{}".format(region_id, BLINK_URL) self.subdomain = 'rest-{}'.format(region_id)
if legacy:
self.subdomain = 'rest.{}'.format(region_id)
self.base_url = "https://{}.{}".format(self.subdomain, BLINK_URL)
self.home_url = "{}/homescreen".format(self.base_url) self.home_url = "{}/homescreen".format(self.base_url)
self.event_url = "{}/events/network".format(self.base_url) self.event_url = "{}/events/network".format(self.base_url)
self.network_url = "{}/network".format(self.base_url) self.network_url = "{}/network".format(self.base_url)
+22 -9
View File
@@ -5,6 +5,7 @@ import logging
from requests.structures import CaseInsensitiveDict from requests.structures import CaseInsensitiveDict
from blinkpy import api from blinkpy import api
from blinkpy.camera import BlinkCamera from blinkpy.camera import BlinkCamera
from blinkpy.helpers.util import time_to_seconds
from blinkpy.helpers.constants import ONLINE from blinkpy.helpers.constants import ONLINE
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -33,6 +34,7 @@ class BlinkSyncModule():
self.network_info = None self.network_info = None
self.events = [] self.events = []
self.cameras = CaseInsensitiveDict({}) self.cameras = CaseInsensitiveDict({})
self.motion_interval = blink.motion_interval
self.motion = {} self.motion = {}
self.last_record = {} self.last_record = {}
self.camera_list = camera_list self.camera_list = camera_list
@@ -80,8 +82,7 @@ class BlinkSyncModule():
def start(self): def start(self):
"""Initialize the system.""" """Initialize the system."""
response = api.request_syncmodule(self.blink, response = api.request_syncmodule(self.blink,
self.network_id, self.network_id)
force=True)
try: try:
self.summary = response['syncmodule'] self.summary = response['syncmodule']
self.network_id = self.summary['network_id'] self.network_id = self.summary['network_id']
@@ -162,27 +163,39 @@ class BlinkSyncModule():
def check_new_videos(self): def check_new_videos(self):
"""Check if new videos since last refresh.""" """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
resp = api.request_videos(self.blink, resp = api.request_videos(self.blink,
time=self.blink.last_refresh, time=interval,
page=0) page=1)
for camera in self.cameras.keys(): for camera in self.cameras.keys():
self.motion[camera] = False self.motion[camera] = False
try: try:
info = resp['videos'] info = resp['media']
except (KeyError, TypeError): except (KeyError, TypeError):
_LOGGER.warning("Could not check for motion. Response: %s", resp) _LOGGER.warning("Could not check for motion. Response: %s", resp)
return False return False
for entry in info: for entry in info:
try: try:
name = entry['camera_name'] name = entry['device_name']
clip = entry['address'] clip = entry['media']
timestamp = entry['created_at'] timestamp = entry['created_at']
self.motion[name] = True if self.check_new_video_time(timestamp):
self.last_record[name] = {'clip': clip, 'time': timestamp} self.motion[name] = True and self.arm
self.last_record[name] = {'clip': clip, 'time': timestamp}
except KeyError: except KeyError:
_LOGGER.debug("No new videos since last refresh.") _LOGGER.debug("No new videos since last refresh.")
return True 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: 0.02
comment: true
require_ci_to_pass: yes
range: 65..90
round: down
precision: 1
+3 -2
View File
@@ -1,3 +1,4 @@
python-dateutil==2.7.5 python-dateutil~=2.8.0
requests>=2.20.0 requests==2.22.0
python-slugify~=3.0.2
testtools==2.3.0 testtools==2.3.0
+9 -8
View File
@@ -1,10 +1,11 @@
flake8==3.5.0 coverage==4.5.4
flake8-docstrings==1.3.0 flake8==3.7.9
pylint==2.3.0 flake8-docstrings==1.5.0
pydocstyle==2.1.1 pylint==2.4.4
pytest==3.7.1 pydocstyle==5.0.2
pytest-cov>=2.3.1 pytest==5.3.5
pytest-sugar>=0.9.0 pytest-cov==2.8.1
pytest-timeout>=1.0.0 pytest-sugar>=0.9.2
pytest-timeout>=1.3.3
restructuredtext-lint>=1.0.1 restructuredtext-lint>=1.0.1
pygments>=2.2.0 pygments>=2.2.0
+10 -8
View File
@@ -69,7 +69,8 @@ class TestBlinkFunctions(unittest.TestCase):
bad_req, bad_req,
new_req new_req
] ]
self.blink.login_request(['test1', 'test2', 'test3']) self.blink.login_urls = ['test1', 'test2', 'test3']
self.blink.login_request()
# pylint: disable=protected-access # pylint: disable=protected-access
self.assertEqual(self.blink._login_url, 'test3') self.assertEqual(self.blink._login_url, 'test3')
@@ -78,7 +79,8 @@ class TestBlinkFunctions(unittest.TestCase):
new_req, new_req,
bad_req bad_req
] ]
self.blink.login_request(['test1', 'test2', 'test3']) self.blink.login_urls = ['test1', 'test2', 'test3']
self.blink.login_request()
# pylint: disable=protected-access # pylint: disable=protected-access
self.assertEqual(self.blink._login_url, 'test2') self.assertEqual(self.blink._login_url, 'test2')
@@ -121,12 +123,12 @@ class TestBlinkFunctions(unittest.TestCase):
blinkpy._LOGGER.setLevel(logging.DEBUG) blinkpy._LOGGER.setLevel(logging.DEBUG)
generic_entry = { generic_entry = {
'created_at': '1970', 'created_at': '1970',
'camera_name': 'foo', 'device_name': 'foo',
'deleted': True, 'deleted': True,
'address': '/bar.mp4' 'media': '/bar.mp4'
} }
result = [generic_entry] result = [generic_entry]
mock_req.return_value = {'videos': result} mock_req.return_value = {'media': result}
blink.last_refresh = 0 blink.last_refresh = 0
formatted_date = get_time(blink.last_refresh) formatted_date = get_time(blink.last_refresh)
expected_log = [ expected_log = [
@@ -147,12 +149,12 @@ class TestBlinkFunctions(unittest.TestCase):
blinkpy._LOGGER.setLevel(logging.DEBUG) blinkpy._LOGGER.setLevel(logging.DEBUG)
generic_entry = { generic_entry = {
'created_at': '1970', 'created_at': '1970',
'camera_name': 'foo', 'device_name': 'foo',
'deleted': True, 'deleted': True,
'address': '/bar.mp4' 'media': '/bar.mp4'
} }
result = [generic_entry] result = [generic_entry]
mock_req.return_value = {'videos': result} mock_req.return_value = {'media': result}
blink.last_refresh = 0 blink.last_refresh = 0
formatted_date = get_time(blink.last_refresh) formatted_date = get_time(blink.last_refresh)
expected_log = [ expected_log = [
+43 -2
View File
@@ -79,11 +79,52 @@ class TestBlinkSetup(unittest.TestCase):
# pylint: disable=protected-access # pylint: disable=protected-access
self.assertEqual(self.blink_no_cred._password, PASSWORD) self.assertEqual(self.blink_no_cred._password, PASSWORD)
@mock.patch('blinkpy.blinkpy.getpass.getpass')
@mock.patch('blinkpy.blinkpy.Blink.get_auth_token')
def test_no_cred_file(self, getpwd, getauth, mock_sess):
"""Check that normal login occurs when cred file doesn't exist."""
# pylint: disable=protected-access
self.blink._cred_file = '/tmp/fake.file'
getpwd.return_value = PASSWORD
getauth.return_value = True
with mock.patch('builtins.input', return_value=USERNAME):
self.assertTrue(self.blink.login())
def test_exit_on_missing_json(self, mock_sess):
"""Test that we fail on missing json data."""
# pylint: disable=protected-access
self.blink._cred_file = '/tmp/fake.file'
with mock.patch('os.path.isfile', return_value=True):
with mock.patch('builtins.open', mock.mock_open(read_data="{}")):
self.assertFalse(self.blink.login())
def test_exit_on_bad_json(self, mock_sess):
"""Test that we fail on bad json format."""
# pylint: disable=protected-access
self.blink._cred_file = '/tmp/fake.file'
with mock.patch('os.path.isfile', return_value=True):
with mock.patch('builtins.open', mock.mock_open(read_data='{]')):
self.assertFalse(self.blink.login())
@mock.patch('blinkpy.blinkpy.json.load')
def test_cred_file(self, mockjson, mock_sess):
"""Test that loading credential file works."""
# pylint: disable=protected-access
self.blink_no_cred._cred_file = '/tmp/fake.file'
mockjson.return_value = {'username': 'foo', 'password': 'bar'}
with mock.patch('os.path.isfile', return_value=True):
with mock.patch('builtins.open', mock.mock_open(read_data='')):
self.assertTrue(self.blink_no_cred.login())
# pylint: disable=protected-access
self.assertEqual(self.blink_no_cred._username, 'foo')
# pylint: disable=protected-access
self.assertEqual(self.blink_no_cred._password, 'bar')
def test_bad_request(self, mock_sess): def test_bad_request(self, mock_sess):
"""Check that we raise an Exception with a bad request.""" """Check that we raise an Exception with a bad request."""
self.blink.session = create_session() self.blink.session = create_session()
explog = ("ERROR:blinkpy.helpers.util:" explog = ("WARNING:blinkpy.helpers.util:"
"Cannot obtain new token for server auth.") "Response from server: 200 - foo")
with self.assertRaises(BlinkException): with self.assertRaises(BlinkException):
http_req(self.blink, reqtype='bad') http_req(self.blink, reqtype='bad')
+4 -43
View File
@@ -87,57 +87,18 @@ class TestBlinkCameraSetup(unittest.TestCase):
self.assertEqual(self.camera.network_id, '5678') self.assertEqual(self.camera.network_id, '5678')
self.assertEqual(self.camera.serial, '12345678') self.assertEqual(self.camera.serial, '12345678')
self.assertEqual(self.camera.motion_enabled, False) self.assertEqual(self.camera.motion_enabled, False)
self.assertEqual(self.camera.battery, 50) self.assertEqual(self.camera.battery, 'ok')
self.assertEqual(self.camera.temperature, 68) self.assertEqual(self.camera.temperature, 68)
self.assertEqual(self.camera.temperature_c, 20) self.assertEqual(self.camera.temperature_c, 20)
self.assertEqual(self.camera.temperature_calibrated, 71) self.assertEqual(self.camera.temperature_calibrated, 71)
self.assertEqual(self.camera.wifi_strength, 4) self.assertEqual(self.camera.wifi_strength, 4)
self.assertEqual(self.camera.thumbnail, self.assertEqual(self.camera.thumbnail,
'https://rest.test.immedia-semi.com/thumb.jpg') 'https://rest-test.immedia-semi.com/thumb.jpg')
self.assertEqual(self.camera.clip, self.assertEqual(self.camera.clip,
'https://rest.test.immedia-semi.com/test.mp4') 'https://rest-test.immedia-semi.com/test.mp4')
self.assertEqual(self.camera.image_from_cache, 'test') self.assertEqual(self.camera.image_from_cache, 'test')
self.assertEqual(self.camera.video_from_cache, 'foobar') self.assertEqual(self.camera.video_from_cache, 'foobar')
def test_thumbnail_not_in_info(self, mock_sess):
"""Test that we grab thumbanil if not in camera_info."""
mock_sess.side_effect = [
mresp.MockResponse({'temp': 71}, 200),
'foobar',
'barfoo'
]
self.camera.last_record = ['1']
self.camera.sync.last_record = {
'new': {
'clip': '/test.mp4',
'time': '1970-01-01T00:00:00'
}
}
config = {
'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': [
{'foo': 'bar'},
{'device_type': 'foobar'},
{'device_type': 'camera',
'name': 'new',
'thumbnail': '/new/thumb'}
]
}
self.camera.update(config)
self.assertEqual(self.camera.thumbnail,
'https://rest.test.immedia-semi.com/new/thumb.jpg')
def test_no_thumbnails(self, mock_sess): def test_no_thumbnails(self, mock_sess):
"""Tests that thumbnail is 'None' if none found.""" """Tests that thumbnail is 'None' if none found."""
mock_sess.return_value = 'foobar' mock_sess.return_value = 'foobar'
@@ -167,7 +128,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
logrecord.output, logrecord.output,
[("WARNING:blinkpy.camera:Could not retrieve calibrated " [("WARNING:blinkpy.camera:Could not retrieve calibrated "
"temperature."), "temperature."),
("ERROR:blinkpy.camera:Could not find thumbnail for camera new" ("WARNING:blinkpy.camera:Could not find thumbnail for camera new"
"\nNoneType: None")] "\nNoneType: None")]
) )
+87 -8
View File
@@ -17,12 +17,14 @@ class TestBlinkSyncModule(unittest.TestCase):
def setUp(self): def setUp(self):
"""Set up Blink module.""" """Set up Blink module."""
self.blink = blinkpy.Blink(username=USERNAME, self.blink = blinkpy.Blink(username=USERNAME,
password=PASSWORD) password=PASSWORD,
motion_interval=0)
# pylint: disable=protected-access # pylint: disable=protected-access
self.blink._auth_header = { self.blink._auth_header = {
'Host': 'test.url.tld', 'Host': 'test.url.tld',
'TOKEN_AUTH': 'foobar123' 'TOKEN_AUTH': 'foobar123'
} }
self.blink.last_refresh = 0
self.blink.urls = blinkpy.BlinkURLHandler('test') self.blink.urls = blinkpy.BlinkURLHandler('test')
self.blink.sync['test'] = BlinkSyncModule(self.blink, self.blink.sync['test'] = BlinkSyncModule(self.blink,
'test', 'test',
@@ -41,6 +43,7 @@ class TestBlinkSyncModule(unittest.TestCase):
None, None,
{'devicestatus': {}}, {'devicestatus': {}},
] ]
self.blink.sync['test'].network_info = {'network': {'armed': True}}
def tearDown(self): def tearDown(self):
"""Clean up after test.""" """Clean up after test."""
@@ -59,29 +62,105 @@ class TestBlinkSyncModule(unittest.TestCase):
self.assertEqual(self.blink.sync['test'].get_camera_info('1234'), self.assertEqual(self.blink.sync['test'].get_camera_info('1234'),
'foobar') 'foobar')
def test_check_new_videos_startup(self, mock_resp):
"""Test that check_new_videos does not block startup."""
sync_module = self.blink.sync['test']
self.blink.last_refresh = None
self.assertFalse(sync_module.check_new_videos())
def test_check_new_videos(self, mock_resp): def test_check_new_videos(self, mock_resp):
"""Test recent video response.""" """Test recent video response."""
mock_resp.return_value = { mock_resp.return_value = {
'videos': [{ 'media': [{
'camera_name': 'foo', 'device_name': 'foo',
'address': '/foo/bar.mp4', 'media': '/foo/bar.mp4',
'created_at': '1970-01-01T00:00:00+0:00' 'created_at': '1990-01-01T00:00:00+00:00'
}] }]
} }
sync_module = self.blink.sync['test'] sync_module = self.blink.sync['test']
sync_module.cameras = {'foo': None} sync_module.cameras = {'foo': None}
sync_module.blink.last_refresh = 0
self.assertEqual(sync_module.motion, {}) self.assertEqual(sync_module.motion, {})
self.assertTrue(sync_module.check_new_videos()) self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.last_record['foo'], self.assertEqual(sync_module.last_record['foo'],
{'clip': '/foo/bar.mp4', {'clip': '/foo/bar.mp4',
'time': '1970-01-01T00:00:00+0:00'}) 'time': '1990-01-01T00:00:00+00:00'})
self.assertEqual(sync_module.motion, {'foo': True}) self.assertEqual(sync_module.motion, {'foo': True})
mock_resp.return_value = {'videos': []} mock_resp.return_value = {'media': []}
self.assertTrue(sync_module.check_new_videos()) self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': False}) self.assertEqual(sync_module.motion, {'foo': False})
self.assertEqual(sync_module.last_record['foo'], self.assertEqual(sync_module.last_record['foo'],
{'clip': '/foo/bar.mp4', {'clip': '/foo/bar.mp4',
'time': '1970-01-01T00:00:00+0:00'}) '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): def test_check_new_videos_failed(self, mock_resp):
"""Test method when response is unexpected.""" """Test method when response is unexpected."""
+15 -1
View File
@@ -3,7 +3,7 @@
import unittest import unittest
from unittest import mock from unittest import mock
import time import time
from blinkpy.helpers.util import Throttle from blinkpy.helpers.util import Throttle, BlinkURLHandler, time_to_seconds
class TestUtil(unittest.TestCase): class TestUtil(unittest.TestCase):
@@ -98,3 +98,17 @@ class TestUtil(unittest.TestCase):
with mock.patch('time.time', return_value=now_plus_6): with mock.patch('time.time', return_value=now_plus_6):
self.assertEqual(tester.test1(), None) self.assertEqual(tester.test1(), None)
self.assertEqual(tester.test2(), True) self.assertEqual(tester.test2(), True)
def test_legacy_subdomains(self):
"""Test that subdomain can be set to legacy mode."""
urls = BlinkURLHandler('test')
self.assertEqual(urls.subdomain, 'rest-test')
urls = BlinkURLHandler('test', legacy=True)
self.assertEqual(urls.subdomain, 'rest.test')
def test_time_to_seconds(self):
"""Test time to seconds conversion."""
correct_time = '1970-01-01T00:00:05+00:00'
wrong_time = '1/1/1970 00:00:03'
self.assertEqual(time_to_seconds(correct_time), 5)
self.assertFalse(time_to_seconds(wrong_time))
+15 -5
View File
@@ -8,11 +8,21 @@ setenv =
LANG=en_US.UTF-8 LANG=en_US.UTF-8
PYTHONPATH = {toxinidir} PYTHONPATH = {toxinidir}
commands = commands =
py.test --timeout=30 --duration=10 --cov=blinkpy --cov-report term-missing {posargs} pytest --timeout=9 --durations=10 --cov=blinkpy --cov-report term-missing {posargs}
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/requirements_test.txt -r{toxinidir}/requirements_test.txt
[testenv:cov]
setenv =
LANG=en_US.UTF-8
PYTHONPATH = {toxinidir}
commands =
pytest --timeout=9 --durations=10 --cov=blinkpy --cov-report=xml {posargs}
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/requirements_test.txt
[testenv:lint] [testenv:lint]
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
@@ -20,9 +30,9 @@ deps =
basepython = python3 basepython = python3
ignore_errors = True ignore_errors = True
commands = commands =
pylint --rcfile={toxinidir}/pylintrc blinkpy tests pylint --rcfile={toxinidir}/pylintrc blinkpy tests app
flake8 blinkpy tests flake8 blinkpy tests app
pydocstyle blinkpy tests pydocstyle blinkpy tests app
rst-lint README.rst rst-lint README.rst
rst-lint CHANGES.rst rst-lint CHANGES.rst