Compare commits
142
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37350da1b2 | ||
|
|
a2555a6c40 | ||
|
|
c01a6ce16b | ||
|
|
ab38afa105 | ||
|
|
ba3c2e3489 | ||
|
|
92a2cc2d9a | ||
|
|
0842739197 | ||
|
|
f09a66c083 | ||
|
|
88984365fe | ||
|
|
469b0c257e | ||
|
|
a94f59eb53 | ||
|
|
eb1fbe90d1 | ||
|
|
b5b7498041 | ||
|
|
828b41197b | ||
|
|
b0fed670b9 | ||
|
|
f13d177615 | ||
|
|
fcce28c9c4 | ||
|
|
e8e9d4e3cd | ||
|
|
59ef398452 | ||
|
|
80e39845ce | ||
|
|
cbab673084 | ||
|
|
f27df12e64 | ||
|
|
bdcb3f6203 | ||
|
|
f619b20e2a | ||
|
|
69aaee3fc4 | ||
|
|
7140895c58 | ||
|
|
190cef875e | ||
|
|
7aef8570a5 | ||
|
|
721a3ad8f2 | ||
|
|
1b1c30bff0 | ||
|
|
8c23087f89 | ||
|
|
dae2413ed1 | ||
|
|
cd9792193f | ||
|
|
75dbd1e498 | ||
|
|
a374009835 | ||
|
|
9a9501635f | ||
|
|
e00600c955 | ||
|
|
141fea542c | ||
|
|
ed4450778d | ||
|
|
19b7eef123 | ||
|
|
41d59b1df0 | ||
|
|
fd104c09da | ||
|
|
6dbed91426 | ||
|
|
cb43f17d75 | ||
|
|
5ebf278ad3 | ||
|
|
6bd8d14190 | ||
|
|
7f782533b8 | ||
|
|
1790c1beae | ||
|
|
09e1c382d3 | ||
|
|
ceb2270ee5 | ||
|
|
986a3c81f3 | ||
|
|
11d6bbb916 | ||
|
|
b02f6318a7 | ||
|
|
c1435e74e8 | ||
|
|
b30da661e5 | ||
|
|
3a22dddc82 | ||
|
|
a19a17d4dc | ||
|
|
bff6ef92ce | ||
|
|
f6ddfbe5cf | ||
|
|
263572eb66 | ||
|
|
3a97a928de | ||
|
|
0322414de1 | ||
|
|
1c75e86863 | ||
|
|
0ae71f6367 | ||
|
|
af7c38695a | ||
|
|
c741c22876 | ||
|
|
06b9b12eeb | ||
|
|
daf7078f98 | ||
|
|
c2db57ecb3 | ||
|
|
1b66eddfb4 | ||
|
|
0336c6a8f8 | ||
|
|
22d3fa9565 | ||
|
|
9d81973b3a | ||
|
|
fe8eac3dc6 | ||
|
|
5fc315201a | ||
|
|
a9d3d5568b | ||
|
|
f88dbdaeba | ||
|
|
48028914de | ||
|
|
fcdb70d246 | ||
|
|
e8589bfa05 | ||
|
|
854ffbf26a | ||
|
|
ca109f703c | ||
|
|
82d5a7ea51 | ||
|
|
79ead7b826 | ||
|
|
6e360b2bb1 | ||
|
|
1aaa5b649f | ||
|
|
435f3b9831 | ||
|
|
bcf0bb5525 | ||
|
|
322aae37dc | ||
|
|
45cb4028b8 | ||
|
|
151d9b7375 | ||
|
|
d393c861d7 | ||
|
|
02c4b2e7e1 | ||
|
|
77959d6d94 | ||
|
|
ad832c4d2d | ||
|
|
0ce2aec139 | ||
|
|
d3761b23ec | ||
|
|
cdbd1a4c27 | ||
|
|
2d2e569595 | ||
|
|
391373b304 | ||
|
|
a030f46b4d | ||
|
|
f6dc22f241 | ||
|
|
220638e525 | ||
|
|
24b1aac1be | ||
|
|
c4608f4258 | ||
|
|
ca19ea62a4 | ||
|
|
2d8f4d8289 | ||
|
|
24ca9d9617 | ||
|
|
41aa1839b7 | ||
|
|
40354e87a0 | ||
|
|
0fc74c3f82 | ||
|
|
7b150eb1af | ||
|
|
f8db8eb3b6 | ||
|
|
a34b6dec67 | ||
|
|
bded3eb156 | ||
|
|
9ca73934ce | ||
|
|
e02b429523 | ||
|
|
22e4b7dabe | ||
|
|
130b5acf84 | ||
|
|
d0fa303c88 | ||
|
|
dff0746450 | ||
|
|
815dfd9a8e | ||
|
|
b0ea0566a5 | ||
|
|
0167900415 | ||
|
|
9279ba2aa9 | ||
|
|
2abfc3e843 | ||
|
|
4ff83585b4 | ||
|
|
3cf2b5e093 | ||
|
|
6a7eff6501 | ||
|
|
3805a41d9f | ||
|
|
f8c78fb499 | ||
|
|
3314ff85ed | ||
|
|
67b24eb89a | ||
|
|
d416e42925 | ||
|
|
17655849ae | ||
|
|
d9e3761c7b | ||
|
|
382db80584 | ||
|
|
695dc96340 | ||
|
|
8b1851b647 | ||
|
|
eeaa9cdf6f | ||
|
|
5cd59c049e | ||
|
|
5d86e0ac9a |
@@ -33,8 +33,9 @@ jobs:
|
||||
run: |
|
||||
tox -r -e cov
|
||||
- name: Codecov
|
||||
uses: codecov/codecov-action@v1.0.6
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
flags: unittests
|
||||
file: ./coverage.xml
|
||||
name: blinkpy
|
||||
|
||||
@@ -8,12 +8,12 @@ repos:
|
||||
- --quiet
|
||||
files: ^((blinkpy|tests)/.+)?[^/]+\.py$
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.8.2
|
||||
rev: 3.8.3
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- flake8-docstrings==1.5.0
|
||||
- pydocstyle==5.0.2
|
||||
- pydocstyle==5.1.1
|
||||
files: ^(blinkpy|tests)/.+\.py$
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks-markup
|
||||
rev: v1.0.0
|
||||
|
||||
+75
@@ -4,6 +4,81 @@ Changelog
|
||||
|
||||
A list of changes between each release
|
||||
|
||||
0.16.4 (2020-11-22)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes:**
|
||||
|
||||
- Updated liveview endpoint (`#389 <https://github.com/fronzbot/blinkpy/pull/389>`__)
|
||||
- Fixed mini thumbnail not updating (`#388 <https://github.com/fronzbot/blinkpy/pull/388>`__)
|
||||
- Add exception catch to prevent NoneType error on refresh, added test to check behavior as well (`#401 <https://github.com/fronzbot/blinkpy/pull/401>`__)
|
||||
- Unrelated: had to add two force methods to refresh for testing purposes. Should not change normal usage.
|
||||
- Fix malformed stream url (`#395 <https://github.com/fronzbot/blinkpy/pull/395>`__)
|
||||
|
||||
**All:**
|
||||
|
||||
- Moved testtools to requirements_test.txt (`#387 <https://github.com/fronzbot/blinkpy/pull/387>`__)
|
||||
- Bumped pytest to 6.1.1
|
||||
- Bumped flake8 to 3.8.4
|
||||
- Fixed README spelling ((`#381 <https://github.com/fronzbot/blinkpy/pull/381>`__) via @rohitsud)
|
||||
- Bumped pygments to 2.7.1
|
||||
- Bumped coverage to 5.3
|
||||
- Bumped pydocstyle to 5.1.1
|
||||
- Bumped pre-commit to 2.7.1
|
||||
- Bumped pylint to 2.6.0
|
||||
- Bumped pytest-cov to 2.10.1
|
||||
|
||||
|
||||
0.16.3 (2020-08-02)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Add user-agent to all headers
|
||||
|
||||
0.16.2 (2020-08-01)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Add user-agent to header at login
|
||||
- Remove extra data parameters at login (not-needed)
|
||||
- Bump pytest to 6.0.1
|
||||
|
||||
|
||||
0.16.1 (2020-07-29)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Unpin requirements, set minimum version instead
|
||||
- Bump coverage to 5.2.1
|
||||
- Bump pytest to 6.0.0
|
||||
|
||||
|
||||
0.16.0 (2020-07-20)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Breaking Changes:**
|
||||
|
||||
- Add arm property to camera, deprecate motion enable method (`#273 <https://github.com/fronzbot/blinkpy/pull/273>`__)
|
||||
- Complete refactoring of auth logic (breaks all pre-0.16.0 setups!) (`#261 <https://github.com/fronzbot/blinkpy/pull/261>`__)
|
||||
|
||||
**New Features:**
|
||||
|
||||
- Add is_errored property to Auth class (`#275 <https://github.com/fronzbot/blinkpy/pull/275>`__)
|
||||
- Add new endpoint to get user infor (`#280 <https://github.com/fronzbot/blinkpy/pull/280>`__)
|
||||
- Add get_liveview command to camera module (`#289 <https://github.com/fronzbot/blinkpy/pull/289>`__)
|
||||
- Add blink Mini Camera support (`#290 <https://github.com/fronzbot/blinkpy/pull/290>`__)
|
||||
- Add option to skip homescreen check (`#305 <https://github.com/fronzbot/blinkpy/pull/305>`__)
|
||||
- Add different timeout for video and image retrieval (`#323 <https://github.com/fronzbot/blinkpy/pull/323>`__)
|
||||
- Modifiy session to use HTTPAdapter and handle retries (`#324 <https://github.com/fronzbot/blinkpy/pull/324>`__)
|
||||
- Add retry option overrides (`#339 <https://github.com/fronzbot/blinkpy/pull/339>`__)
|
||||
|
||||
**All changes:**
|
||||
|
||||
Please see the change list in the (`Release Notes <https://github.com/fronzbot/releases/tag/v0.16.0>`__)
|
||||
|
||||
|
||||
0.15.1 (2020-07-11)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- Bugfix: remove "Host" from auth header (`#330 <https://github.com/fronzbot/blinkpy/pull/330>`__)
|
||||
|
||||
|
||||
0.15.0 (2020-05-08)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
**Breaking Changes:**
|
||||
|
||||
+1
-1
@@ -174,7 +174,7 @@ Similar methods exist for individual cameras:
|
||||
|
||||
Download videos
|
||||
----------------
|
||||
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.
|
||||
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 ``stop=`` variable. Note that by default, the library will search the first ten pages which is sufficient in most use cases. Additionally, you can specify one or more cameras via the ``camera=`` property. This can be a single string indicating the name of the camera, or a list of camera names. By default, it is set to the string ``'all'`` to grab videos from all cameras.
|
||||
|
||||
Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34am to the ``/home/blink`` directory:
|
||||
|
||||
|
||||
+14
-6
@@ -1,11 +1,12 @@
|
||||
"""Script to run blinkpy as an app."""
|
||||
from os import environ
|
||||
from datetime import datetime, timedelta
|
||||
from blinkpy import blinkpy
|
||||
from blinkpy.blinkpy import Blink
|
||||
from blinkpy.auth import Auth
|
||||
from blinkpy.helpers.util import json_load
|
||||
|
||||
|
||||
USERNAME = environ.get("USERNAME")
|
||||
PASSWORD = environ.get("PASSWORD")
|
||||
CREDFILE = environ.get("CREDFILE")
|
||||
TIMEDELTA = timedelta(environ.get("TIMEDELTA", 1))
|
||||
|
||||
|
||||
@@ -21,11 +22,18 @@ def download_videos(blink, save_dir="/media"):
|
||||
|
||||
def start():
|
||||
"""Startup blink app."""
|
||||
blink = blinkpy.Blink(username=USERNAME, password=PASSWORD)
|
||||
blink = Blink()
|
||||
blink.auth = Auth(json_load(CREDFILE))
|
||||
blink.start()
|
||||
return blink
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the app."""
|
||||
blink = start()
|
||||
download_videos(blink)
|
||||
blink.save(CREDFILE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
BLINK = start()
|
||||
download_videos(BLINK)
|
||||
main()
|
||||
|
||||
+12
-11
@@ -3,7 +3,7 @@
|
||||
import logging
|
||||
from json import dumps
|
||||
from blinkpy.helpers.util import get_time, Throttle
|
||||
from blinkpy.helpers.constants import DEFAULT_URL
|
||||
from blinkpy.helpers.constants import DEFAULT_URL, TIMEOUT, DEFAULT_USER_AGENT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -20,21 +20,24 @@ def request_login(
|
||||
:param url: Login url.
|
||||
:login_data: Dictionary containing blink login data.
|
||||
"""
|
||||
headers = {"Host": DEFAULT_URL, "Content-Type": "application/json"}
|
||||
headers = {
|
||||
"Host": DEFAULT_URL,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "/",
|
||||
"user-agent": DEFAULT_USER_AGENT,
|
||||
}
|
||||
data = dumps(
|
||||
{
|
||||
"email": login_data["username"],
|
||||
"password": login_data["password"],
|
||||
"notification_key": login_data["notification_key"],
|
||||
"unique_id": login_data["uid"],
|
||||
"app_version": "6.0.7 (520300) #afb0be72a",
|
||||
"device_identifier": login_data["device_id"],
|
||||
"client_name": "Computer",
|
||||
"client_type": "android",
|
||||
"os_version": "5.1.1",
|
||||
"reauth": "true",
|
||||
"reauth": "false",
|
||||
}
|
||||
)
|
||||
|
||||
return auth.query(
|
||||
url=url,
|
||||
headers=headers,
|
||||
@@ -241,9 +244,7 @@ def request_camera_liveview(blink, network, camera_id):
|
||||
:param network: Sync module network id.
|
||||
:param camera_id: Camera ID of camera to request liveview from.
|
||||
"""
|
||||
url = (
|
||||
f"{blink.urls.base_url}/api/v3/networks/{network}/cameras/{camera_id}/liveview"
|
||||
)
|
||||
url = f"{blink.urls.base_url}/api/v5/accounts/{blink.account_id}/networks/{network}/cameras/{camera_id}/liveview"
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
@@ -284,7 +285,7 @@ def request_motion_detection_disable(blink, network, camera_id):
|
||||
return http_post(blink, url)
|
||||
|
||||
|
||||
def http_get(blink, url, stream=False, json=True, is_retry=False):
|
||||
def http_get(blink, url, stream=False, json=True, is_retry=False, timeout=TIMEOUT):
|
||||
"""
|
||||
Perform an http get request.
|
||||
|
||||
@@ -304,7 +305,7 @@ def http_get(blink, url, stream=False, json=True, is_retry=False):
|
||||
)
|
||||
|
||||
|
||||
def http_post(blink, url, is_retry=False):
|
||||
def http_post(blink, url, is_retry=False, timeout=TIMEOUT):
|
||||
"""
|
||||
Perform an http post request.
|
||||
|
||||
|
||||
+48
-16
@@ -2,9 +2,16 @@
|
||||
import logging
|
||||
from functools import partial
|
||||
from requests import Request, Session, exceptions
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
from blinkpy import api
|
||||
from blinkpy.helpers import util
|
||||
from blinkpy.helpers.constants import BLINK_URL, LOGIN_ENDPOINT
|
||||
from blinkpy.helpers.constants import (
|
||||
BLINK_URL,
|
||||
DEFAULT_USER_AGENT,
|
||||
LOGIN_ENDPOINT,
|
||||
TIMEOUT,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,12 +58,27 @@ class Auth:
|
||||
"""Return authorization header."""
|
||||
if self.token is None:
|
||||
return None
|
||||
return {"Host": self.host, "TOKEN_AUTH": self.token}
|
||||
return {"TOKEN_AUTH": self.token, "user-agent": DEFAULT_USER_AGENT}
|
||||
|
||||
def create_session(self):
|
||||
def create_session(self, opts=None):
|
||||
"""Create a session for blink communication."""
|
||||
if opts is None:
|
||||
opts = {}
|
||||
backoff = opts.get("backoff", 1)
|
||||
retries = opts.get("retries", 3)
|
||||
retry_list = opts.get("retry_list", [429, 500, 502, 503, 504])
|
||||
sess = Session()
|
||||
sess.get = partial(sess.get, timeout=10)
|
||||
assert_status_hook = [
|
||||
lambda response, *args, **kwargs: response.raise_for_status()
|
||||
]
|
||||
sess.hooks["response"] = assert_status_hook
|
||||
retry = Retry(
|
||||
total=retries, backoff_factor=backoff, status_forcelist=retry_list
|
||||
)
|
||||
adapter = HTTPAdapter(max_retries=retry)
|
||||
sess.mount("https://", adapter)
|
||||
sess.mount("http://", adapter)
|
||||
sess.get = partial(sess.get, timeout=TIMEOUT)
|
||||
return sess
|
||||
|
||||
def prepare_request(self, url, headers, data, reqtype):
|
||||
@@ -91,11 +113,7 @@ class Auth:
|
||||
try:
|
||||
_LOGGER.info("Token expired, attempting automatic refresh.")
|
||||
self.login_response = self.login()
|
||||
self.region_id = self.login_response["region"]["tier"]
|
||||
self.host = f"{self.region_id}.{BLINK_URL}"
|
||||
self.token = self.login_response["authtoken"]["authtoken"]
|
||||
self.client_id = self.login_response["client"]["id"]
|
||||
self.account_id = self.login_response["account"]["id"]
|
||||
self.extract_login_info()
|
||||
self.is_errored = False
|
||||
except LoginError:
|
||||
_LOGGER.error("Login endpoint failed. Try again later.")
|
||||
@@ -105,6 +123,14 @@ class Auth:
|
||||
raise TokenRefreshFailed
|
||||
return True
|
||||
|
||||
def extract_login_info(self):
|
||||
"""Extract login info from login response."""
|
||||
self.region_id = self.login_response["region"]["tier"]
|
||||
self.host = f"{self.region_id}.{BLINK_URL}"
|
||||
self.token = self.login_response["authtoken"]["authtoken"]
|
||||
self.client_id = self.login_response["client"]["id"]
|
||||
self.account_id = self.login_response["account"]["id"]
|
||||
|
||||
def startup(self):
|
||||
"""Initialize tokens for communication."""
|
||||
self.validate_login()
|
||||
@@ -140,6 +166,7 @@ class Auth:
|
||||
stream=False,
|
||||
json_resp=True,
|
||||
is_retry=False,
|
||||
timeout=TIMEOUT,
|
||||
):
|
||||
"""
|
||||
Perform server requests.
|
||||
@@ -154,21 +181,25 @@ class Auth:
|
||||
"""
|
||||
req = self.prepare_request(url, headers, data, reqtype)
|
||||
try:
|
||||
response = self.session.send(req, stream=stream)
|
||||
response = self.session.send(req, stream=stream, timeout=timeout)
|
||||
return self.validate_response(response, json_resp)
|
||||
except (exceptions.ConnectionError, exceptions.Timeout):
|
||||
_LOGGER.error(
|
||||
"Connection error. Endpoint %s possibly down or throttled. %s: %s",
|
||||
url,
|
||||
response.status_code,
|
||||
response.reason,
|
||||
"Connection error. Endpoint %s possibly down or throttled.", url,
|
||||
)
|
||||
except BlinkBadResponse:
|
||||
code = None
|
||||
reason = None
|
||||
try:
|
||||
code = response.status_code
|
||||
reason = response.reason
|
||||
except AttributeError:
|
||||
pass
|
||||
_LOGGER.error(
|
||||
"Expected json response from %s, but received: %s: %s",
|
||||
url,
|
||||
response.status_code,
|
||||
response.reason,
|
||||
code,
|
||||
reason,
|
||||
)
|
||||
except UnauthorizedError:
|
||||
try:
|
||||
@@ -182,6 +213,7 @@ class Auth:
|
||||
stream=stream,
|
||||
json_resp=json_resp,
|
||||
is_retry=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
_LOGGER.error("Unable to access %s after token refresh.", url)
|
||||
except TokenRefreshFailed:
|
||||
|
||||
+43
-12
@@ -29,6 +29,7 @@ from blinkpy.helpers.constants import (
|
||||
DEFAULT_MOTION_INTERVAL,
|
||||
DEFAULT_REFRESH,
|
||||
MIN_THROTTLE_TIME,
|
||||
TIMEOUT_MEDIA,
|
||||
)
|
||||
from blinkpy.helpers.constants import __version__
|
||||
from blinkpy.auth import Auth, TokenRefreshFailed, LoginError
|
||||
@@ -40,7 +41,10 @@ class Blink:
|
||||
"""Class to initialize communication."""
|
||||
|
||||
def __init__(
|
||||
self, refresh_rate=DEFAULT_REFRESH, motion_interval=DEFAULT_MOTION_INTERVAL,
|
||||
self,
|
||||
refresh_rate=DEFAULT_REFRESH,
|
||||
motion_interval=DEFAULT_MOTION_INTERVAL,
|
||||
no_owls=False,
|
||||
):
|
||||
"""
|
||||
Initialize Blink system.
|
||||
@@ -51,6 +55,7 @@ class Blink:
|
||||
Defaults to last refresh time.
|
||||
Useful for preventing motion_detected property
|
||||
from de-asserting too quickly.
|
||||
:param no_owls: Disable searching for owl entries (blink mini cameras only known entity). Prevents an uneccessary API call if you don't have these in your network.
|
||||
"""
|
||||
self.auth = Auth()
|
||||
self.account_id = None
|
||||
@@ -68,22 +73,25 @@ class Blink:
|
||||
self.available = False
|
||||
self.key_required = False
|
||||
self.homescreen = {}
|
||||
self.no_owls = no_owls
|
||||
|
||||
@util.Throttle(seconds=MIN_THROTTLE_TIME)
|
||||
def refresh(self, force=False):
|
||||
def refresh(self, force=False, force_cache=False):
|
||||
"""
|
||||
Perform a system refresh.
|
||||
|
||||
:param force: Force an update of the camera data
|
||||
:param force: Used to override throttle, resets refresh
|
||||
:param force_cache: Used to force update without overriding throttle
|
||||
"""
|
||||
if self.check_if_ok_to_update() or force:
|
||||
if self.check_if_ok_to_update() or force or force_cache:
|
||||
if not self.available:
|
||||
self.setup_post_verify()
|
||||
|
||||
self.get_homescreen()
|
||||
for sync_name, sync_module in self.sync.items():
|
||||
_LOGGER.debug("Attempting refresh of sync %s", sync_name)
|
||||
sync_module.refresh(force_cache=force)
|
||||
if not force:
|
||||
sync_module.refresh(force_cache=(force or force_cache))
|
||||
if not force_cache:
|
||||
# Prevents rapid clearing of motion detect property
|
||||
self.last_refresh = int(time.time())
|
||||
return True
|
||||
@@ -95,6 +103,7 @@ class Blink:
|
||||
self.auth.startup()
|
||||
self.setup_login_ids()
|
||||
self.setup_urls()
|
||||
self.get_homescreen()
|
||||
except (LoginError, TokenRefreshFailed, BlinkSetupError):
|
||||
_LOGGER.error("Cannot setup Blink platform.")
|
||||
self.available = False
|
||||
@@ -128,7 +137,6 @@ class Blink:
|
||||
sync_cameras = cameras.get(network_id, {})
|
||||
self.setup_sync_module(name, network_id, sync_cameras)
|
||||
|
||||
self.setup_owls()
|
||||
self.cameras = self.merge_cameras()
|
||||
|
||||
self.available = True
|
||||
@@ -140,15 +148,27 @@ class Blink:
|
||||
self.sync[name] = BlinkSyncModule(self, name, network_id, cameras)
|
||||
self.sync[name].start()
|
||||
|
||||
def get_homescreen(self):
|
||||
"""Get homecreen information."""
|
||||
if self.no_owls:
|
||||
_LOGGER.debug("Skipping owl extraction.")
|
||||
self.homescreen = {}
|
||||
return
|
||||
self.homescreen = api.request_homescreen(self)
|
||||
|
||||
def setup_owls(self):
|
||||
"""Check for mini cameras."""
|
||||
response = api.request_homescreen(self)
|
||||
self.homescreen = response
|
||||
network_list = []
|
||||
camera_list = []
|
||||
try:
|
||||
for owl in response["owls"]:
|
||||
for owl in self.homescreen["owls"]:
|
||||
name = owl["name"]
|
||||
network_id = owl["network_id"]
|
||||
network_id = str(owl["network_id"])
|
||||
if network_id in self.network_ids:
|
||||
camera_list.append(
|
||||
{network_id: {"name": name, "id": network_id, "type": "mini"}}
|
||||
)
|
||||
continue
|
||||
if owl["onboarded"]:
|
||||
network_list.append(str(network_id))
|
||||
self.sync[name] = BlinkOwl(self, name, network_id, owl)
|
||||
@@ -158,6 +178,7 @@ class Blink:
|
||||
pass
|
||||
|
||||
self.network_ids.extend(network_list)
|
||||
return camera_list
|
||||
|
||||
def setup_camera_list(self):
|
||||
"""Create camera list for onboarded networks."""
|
||||
@@ -172,6 +193,10 @@ class Blink:
|
||||
all_cameras[camera_network].append(
|
||||
{"name": camera["name"], "id": camera["id"]}
|
||||
)
|
||||
mini_cameras = self.setup_owls()
|
||||
for camera in mini_cameras:
|
||||
for network, camera_info in camera.items():
|
||||
all_cameras[network].append(camera_info)
|
||||
return all_cameras
|
||||
except (KeyError, TypeError):
|
||||
_LOGGER.error("Unable to retrieve cameras from response %s", response)
|
||||
@@ -308,7 +333,13 @@ class Blink:
|
||||
_LOGGER.info("%s already exists, skipping...", filename)
|
||||
continue
|
||||
|
||||
response = api.http_get(self, url=clip_address, stream=True, json=False)
|
||||
response = api.http_get(
|
||||
self,
|
||||
url=clip_address,
|
||||
stream=True,
|
||||
json=False,
|
||||
timeout=TIMEOUT_MEDIA,
|
||||
)
|
||||
with open(filename, "wb") as vidfile:
|
||||
copyfileobj(response.raw, vidfile)
|
||||
|
||||
|
||||
+26
-13
@@ -3,6 +3,7 @@
|
||||
from shutil import copyfileobj
|
||||
import logging
|
||||
from blinkpy import api
|
||||
from blinkpy.helpers.constants import TIMEOUT_MEDIA
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,6 +30,7 @@ class BlinkCamera:
|
||||
self.last_record = None
|
||||
self._cached_image = None
|
||||
self._cached_video = None
|
||||
self.camera_type = ""
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
@@ -96,6 +98,15 @@ class BlinkCamera:
|
||||
self.sync.blink, self.network_id, self.camera_id
|
||||
)
|
||||
|
||||
def get_media(self, media_type="image"):
|
||||
"""Download media (image or video)."""
|
||||
url = self.thumbnail
|
||||
if media_type.lower() == "video":
|
||||
url = self.clip
|
||||
return api.http_get(
|
||||
self.sync.blink, url=url, stream=True, json=False, timeout=TIMEOUT_MEDIA,
|
||||
)
|
||||
|
||||
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)
|
||||
@@ -149,9 +160,7 @@ class BlinkCamera:
|
||||
if config.get("thumbnail", False):
|
||||
thumb_addr = config["thumbnail"]
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Could not find thumbnail for camera %s", self.name, exc_info=True
|
||||
)
|
||||
_LOGGER.warning("Could not find thumbnail for camera %s", self.name)
|
||||
|
||||
if thumb_addr is not None:
|
||||
new_thumbnail = f"{self.sync.urls.base_url}{thumb_addr}.jpg"
|
||||
@@ -180,13 +189,10 @@ 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 = self.get_media()
|
||||
|
||||
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
|
||||
)
|
||||
self._cached_video = self.get_media(media_type="video")
|
||||
|
||||
def get_liveview(self):
|
||||
"""Get livewview rtsps link."""
|
||||
@@ -202,7 +208,7 @@ class BlinkCamera:
|
||||
:param path: Path to write file
|
||||
"""
|
||||
_LOGGER.debug("Writing image from %s to %s", self.name, path)
|
||||
response = self._cached_image
|
||||
response = self.get_media()
|
||||
if response.status_code == 200:
|
||||
with open(path, "wb") as imgfile:
|
||||
copyfileobj(response.raw, imgfile)
|
||||
@@ -218,7 +224,7 @@ class BlinkCamera:
|
||||
:param path: Path to write file
|
||||
"""
|
||||
_LOGGER.debug("Writing video from %s to %s", self.name, path)
|
||||
response = self._cached_video
|
||||
response = self.get_media(media_type="video")
|
||||
if response is None:
|
||||
_LOGGER.error("No saved video exist for %s.", self.name)
|
||||
return
|
||||
@@ -229,6 +235,11 @@ class BlinkCamera:
|
||||
class BlinkCameraMini(BlinkCamera):
|
||||
"""Define a class for a Blink Mini camera."""
|
||||
|
||||
def __init__(self, sync):
|
||||
"""Initialize a Blink Mini cameras."""
|
||||
super().__init__(sync)
|
||||
self.camera_type = "mini"
|
||||
|
||||
@property
|
||||
def arm(self):
|
||||
"""Return camera arm status."""
|
||||
@@ -237,7 +248,9 @@ class BlinkCameraMini(BlinkCamera):
|
||||
@arm.setter
|
||||
def arm(self, value):
|
||||
"""Set camera arm status."""
|
||||
self.sync.arm = value
|
||||
_LOGGER.warning(
|
||||
"Individual camera motion detection enable/disable for Blink Mini cameras is unsupported at this time."
|
||||
)
|
||||
|
||||
def snap_picture(self):
|
||||
"""Snap picture for a blink mini camera."""
|
||||
@@ -253,6 +266,6 @@ class BlinkCameraMini(BlinkCamera):
|
||||
response = api.http_post(self.sync.blink, url)
|
||||
server = response["server"]
|
||||
server_split = server.split(":")
|
||||
server_split[0] = "rtsps"
|
||||
server_split[0] = "rtsps:"
|
||||
link = "".join(server_split)
|
||||
return link
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 16
|
||||
PATCH_VERSION = "0-rc4"
|
||||
PATCH_VERSION = 4
|
||||
|
||||
__version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}"
|
||||
|
||||
@@ -58,6 +58,7 @@ ONLINE = {"online": True, "offline": False}
|
||||
"""
|
||||
OTHER
|
||||
"""
|
||||
DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"
|
||||
DEVICE_ID = "Blinkpy"
|
||||
TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
|
||||
DEFAULT_MOTION_INTERVAL = 1
|
||||
@@ -65,3 +66,5 @@ DEFAULT_REFRESH = 30
|
||||
MIN_THROTTLE_TIME = 2
|
||||
SIZE_NOTIFICATION_KEY = 152
|
||||
SIZE_UID = 16
|
||||
TIMEOUT = 10
|
||||
TIMEOUT_MEDIA = 90
|
||||
|
||||
+32
-12
@@ -93,9 +93,7 @@ class BlinkSyncModule:
|
||||
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
|
||||
)
|
||||
_LOGGER.error("Could not extract some sync module info: %s", response)
|
||||
|
||||
is_ok = self.get_network_info()
|
||||
self.check_new_videos()
|
||||
@@ -124,16 +122,33 @@ class BlinkSyncModule:
|
||||
for camera_config in self.camera_list:
|
||||
if "name" not in camera_config:
|
||||
break
|
||||
blink_camera_type = camera_config.get("type", "")
|
||||
name = camera_config["name"]
|
||||
self.cameras[name] = camera_type(self)
|
||||
self.motion[name] = False
|
||||
camera_info = self.get_camera_info(camera_config["id"])
|
||||
owl_info = self.get_owl_info(name)
|
||||
if blink_camera_type == "mini":
|
||||
camera_type = BlinkCameraMini
|
||||
self.cameras[name] = camera_type(self)
|
||||
camera_info = self.get_camera_info(
|
||||
camera_config["id"], owl_info=owl_info
|
||||
)
|
||||
self.cameras[name].update(camera_info, force_cache=True, force=True)
|
||||
|
||||
except KeyError:
|
||||
_LOGGER.error("Could not create camera instances for %s", self.name)
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_owl_info(self, name):
|
||||
"""Extract owl information."""
|
||||
try:
|
||||
for owl in self.blink.homescreen["owls"]:
|
||||
if owl["name"] == name:
|
||||
return owl
|
||||
except (TypeError, KeyError):
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_events(self, **kwargs):
|
||||
"""Retrieve events from server."""
|
||||
force = kwargs.pop("force", False)
|
||||
@@ -141,17 +156,20 @@ class BlinkSyncModule:
|
||||
try:
|
||||
return response["event"]
|
||||
except (TypeError, KeyError):
|
||||
_LOGGER.error("Could not extract events: %s", response, exc_info=True)
|
||||
_LOGGER.error("Could not extract events: %s", response)
|
||||
return False
|
||||
|
||||
def get_camera_info(self, camera_id):
|
||||
def get_camera_info(self, camera_id, **kwargs):
|
||||
"""Retrieve camera information."""
|
||||
owl = kwargs.get("owl_info", None)
|
||||
if owl is not None:
|
||||
return owl
|
||||
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 []
|
||||
_LOGGER.error("Could not extract camera info: %s", response)
|
||||
return {}
|
||||
|
||||
def get_network_info(self):
|
||||
"""Retrieve network status."""
|
||||
@@ -171,7 +189,9 @@ class BlinkSyncModule:
|
||||
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)
|
||||
camera_info = self.get_camera_info(
|
||||
camera_id, owl_info=self.get_owl_info(camera_name)
|
||||
)
|
||||
self.cameras[camera_name].update(camera_info, force_cache=force_cache)
|
||||
self.available = True
|
||||
|
||||
@@ -243,14 +263,14 @@ class BlinkOwl(BlinkSyncModule):
|
||||
"""Update sync-less cameras."""
|
||||
return super().update_cameras(camera_type=BlinkCameraMini)
|
||||
|
||||
def get_camera_info(self, camera_id):
|
||||
def get_camera_info(self, camera_id, **kwargs):
|
||||
"""Retrieve camera information."""
|
||||
try:
|
||||
for owl in self.blink.homescreen["owls"]:
|
||||
if owl["name"] == self.name:
|
||||
self.status = owl["enabled"]
|
||||
return owl
|
||||
except KeyError:
|
||||
except (TypeError, KeyError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
+6
-2
@@ -4,8 +4,12 @@ coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 80
|
||||
threshold: 0.02
|
||||
target: 80%
|
||||
threshold: 2%
|
||||
patch:
|
||||
default:
|
||||
target: 60%
|
||||
threshold: 5%
|
||||
|
||||
comment: true
|
||||
require_ci_to_pass: yes
|
||||
|
||||
@@ -30,6 +30,29 @@ By default, the ``blink.auth.Auth`` class creates its own websession via its ``c
|
||||
blink.auth = Auth()
|
||||
blink.auth.session = YourCustomSession
|
||||
|
||||
|
||||
Custom Retry Logic
|
||||
--------------------
|
||||
The built-in auth session via the ``create_session`` method allows for customizable retry intervals and conditions. These parameters are:
|
||||
|
||||
- retries
|
||||
- backoff
|
||||
- retry_list
|
||||
|
||||
``retries`` is the total number of retry attempts that each http request can do before timing out. ``backoff`` is a parameter that allows for non-linear retry times such that the time between retries is backoff*(2^(retries) - 1). ``retry_list`` is simply a list of status codes to force a retry. By default ``retries=3``, ``backoff=1``, and ``retry_list=[429, 500, 502, 503, 504]``. To override them, you need to add you overrides to a dictionary and use that to create a new session with the ``opts`` variable in the ``create_session`` method. The following example can serve as a guide where only the number of retries and backoff factor are overridden:
|
||||
|
||||
.. code:: python
|
||||
|
||||
from blinkpy.blinkpy import Blink
|
||||
from blinkpy.auth import Auth
|
||||
|
||||
blink = Blink()
|
||||
blink.auth = Auth()
|
||||
|
||||
opts = {"retries": 10, "backoff": 2}
|
||||
blink.auth.session = blink.auth.create_session(opts=opts)
|
||||
|
||||
|
||||
Custom HTTP requests
|
||||
---------------------
|
||||
In addition to custom sessions, custom blink server requests can be performed. This give you the ability to bypass the built-in ``Auth.query`` method. It also allows flexibility by giving you the option to pass your own url, rather than be limited to what is currently implemented in the ``blinkpy.api`` module.
|
||||
|
||||
+3
-4
@@ -1,4 +1,3 @@
|
||||
python-dateutil~=2.8.1
|
||||
requests~=2.23.0
|
||||
python-slugify~=4.0.0
|
||||
testtools==2.4.0
|
||||
python-dateutil>=2.8.1
|
||||
requests>=2.24.0
|
||||
python-slugify>=4.0.1
|
||||
|
||||
+11
-10
@@ -1,13 +1,14 @@
|
||||
black==19.10b0
|
||||
coverage==5.1
|
||||
flake8==3.8.3
|
||||
coverage==5.3
|
||||
flake8==3.8.4
|
||||
flake8-docstrings==1.5.0
|
||||
pre-commit==2.5.1
|
||||
pylint==2.5.3
|
||||
pydocstyle==5.0.2
|
||||
pytest==5.4.3
|
||||
pytest-cov==2.9.0
|
||||
pytest-sugar==0.9.3
|
||||
pytest-timeout==1.3.4
|
||||
pre-commit==2.7.1
|
||||
pylint==2.6.0
|
||||
pydocstyle==5.1.1
|
||||
pytest==6.1.1
|
||||
pytest-cov==2.10.1
|
||||
pytest-sugar==0.9.4
|
||||
pytest-timeout==1.4.2
|
||||
restructuredtext-lint==1.3.1
|
||||
pygments==2.6.1
|
||||
pygments==2.7.1
|
||||
testtools>=2.4.0
|
||||
|
||||
@@ -3,23 +3,26 @@
|
||||
from os.path import abspath, dirname
|
||||
from setuptools import setup, find_packages
|
||||
from blinkpy.helpers.constants import (
|
||||
__version__, PROJECT_PACKAGE_NAME, PROJECT_LICENSE, PROJECT_URL,
|
||||
PROJECT_EMAIL, PROJECT_DESCRIPTION, PROJECT_CLASSIFIERS, PROJECT_AUTHOR,
|
||||
__version__,
|
||||
PROJECT_PACKAGE_NAME,
|
||||
PROJECT_LICENSE,
|
||||
PROJECT_URL,
|
||||
PROJECT_EMAIL,
|
||||
PROJECT_DESCRIPTION,
|
||||
PROJECT_CLASSIFIERS,
|
||||
PROJECT_AUTHOR,
|
||||
)
|
||||
|
||||
PROJECT_VERSION = __version__
|
||||
|
||||
THIS_DIR = abspath(dirname(__file__))
|
||||
|
||||
REQUIRES = [
|
||||
"python-dateutil~=2.8.1",
|
||||
"requests~=2.23.0",
|
||||
"python-slugify~=4.0.0",
|
||||
]
|
||||
with open(f"{THIS_DIR}/requirements.txt") as req_file:
|
||||
REQUIRES = [line.rstrip() for line in req_file]
|
||||
|
||||
PACKAGES = find_packages(exclude=['tests*', 'docs'])
|
||||
PACKAGES = find_packages(exclude=["tests*", "docs"])
|
||||
|
||||
with open('{}/README.rst'.format(THIS_DIR), encoding='utf-8') as readme_file:
|
||||
with open("{}/README.rst".format(THIS_DIR), encoding="utf-8") as readme_file:
|
||||
LONG_DESCRIPTION = readme_file.read()
|
||||
|
||||
setup(
|
||||
@@ -31,11 +34,11 @@ setup(
|
||||
author_email=PROJECT_EMAIL,
|
||||
license=PROJECT_LICENSE,
|
||||
url=PROJECT_URL,
|
||||
platforms='any',
|
||||
py_modules=['blinkpy'],
|
||||
platforms="any",
|
||||
py_modules=["blinkpy"],
|
||||
packages=PACKAGES,
|
||||
include_package_data=True,
|
||||
install_requires=REQUIRES,
|
||||
test_suite='tests',
|
||||
classifiers=PROJECT_CLASSIFIERS
|
||||
test_suite="tests",
|
||||
classifiers=PROJECT_CLASSIFIERS,
|
||||
)
|
||||
|
||||
+45
-3
@@ -128,8 +128,7 @@ class TestAuth(unittest.TestCase):
|
||||
def test_header(self):
|
||||
"""Test header data."""
|
||||
self.auth.token = "bar"
|
||||
self.auth.host = "foo"
|
||||
expected_header = {"Host": "foo", "TOKEN_AUTH": "bar"}
|
||||
expected_header = {"TOKEN_AUTH": "bar", "user-agent": const.DEFAULT_USER_AGENT}
|
||||
self.assertDictEqual(self.auth.header, expected_header)
|
||||
|
||||
def test_header_no_token(self):
|
||||
@@ -231,7 +230,7 @@ class TestAuth(unittest.TestCase):
|
||||
@mock.patch("blinkpy.auth.Auth.refresh_token")
|
||||
def test_query_retry_failed(self, mock_refresh, mock_validate):
|
||||
"""Check handling of failed retry request."""
|
||||
self.auth.seession = MockSession()
|
||||
self.auth.session = MockSession()
|
||||
mock_validate.side_effect = [UnauthorizedError, BlinkBadResponse]
|
||||
mock_refresh.return_value = True
|
||||
self.assertEqual(self.auth.query(url="http://example.com"), None)
|
||||
@@ -239,6 +238,49 @@ class TestAuth(unittest.TestCase):
|
||||
mock_validate.side_effect = [UnauthorizedError, TokenRefreshFailed]
|
||||
self.assertEqual(self.auth.query(url="http://example.com"), None)
|
||||
|
||||
def test_default_session(self):
|
||||
"""Test default session creation."""
|
||||
sess = self.auth.create_session()
|
||||
adapter = sess.adapters["https://"]
|
||||
self.assertEqual(adapter.max_retries.total, 3)
|
||||
self.assertEqual(adapter.max_retries.backoff_factor, 1)
|
||||
self.assertEqual(
|
||||
adapter.max_retries.status_forcelist, [429, 500, 502, 503, 504]
|
||||
)
|
||||
|
||||
def test_custom_session_full(self):
|
||||
"""Test full custom session creation."""
|
||||
opts = {"backoff": 2, "retries": 10, "retry_list": [404]}
|
||||
sess = self.auth.create_session(opts=opts)
|
||||
adapter = sess.adapters["https://"]
|
||||
self.assertEqual(adapter.max_retries.total, 10)
|
||||
self.assertEqual(adapter.max_retries.backoff_factor, 2)
|
||||
self.assertEqual(adapter.max_retries.status_forcelist, [404])
|
||||
|
||||
def test_custom_session_partial(self):
|
||||
"""Test partial custom session creation."""
|
||||
opts1 = {"backoff": 2}
|
||||
opts2 = {"retries": 5}
|
||||
opts3 = {"retry_list": [101, 202]}
|
||||
sess1 = self.auth.create_session(opts=opts1)
|
||||
sess2 = self.auth.create_session(opts=opts2)
|
||||
sess3 = self.auth.create_session(opts=opts3)
|
||||
adapt1 = sess1.adapters["https://"]
|
||||
adapt2 = sess2.adapters["https://"]
|
||||
adapt3 = sess3.adapters["https://"]
|
||||
|
||||
self.assertEqual(adapt1.max_retries.total, 3)
|
||||
self.assertEqual(adapt1.max_retries.backoff_factor, 2)
|
||||
self.assertEqual(adapt1.max_retries.status_forcelist, [429, 500, 502, 503, 504])
|
||||
|
||||
self.assertEqual(adapt2.max_retries.total, 5)
|
||||
self.assertEqual(adapt2.max_retries.backoff_factor, 1)
|
||||
self.assertEqual(adapt2.max_retries.status_forcelist, [429, 500, 502, 503, 504])
|
||||
|
||||
self.assertEqual(adapt3.max_retries.total, 3)
|
||||
self.assertEqual(adapt3.max_retries.backoff_factor, 1)
|
||||
self.assertEqual(adapt3.max_retries.status_forcelist, [101, 202])
|
||||
|
||||
|
||||
class MockSession:
|
||||
"""Object to mock a session."""
|
||||
|
||||
@@ -5,29 +5,23 @@ import logging
|
||||
|
||||
from blinkpy import blinkpy
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.camera import BlinkCamera
|
||||
from blinkpy.helpers.util import get_time, BlinkURLHandler
|
||||
|
||||
|
||||
class MockSyncModule(BlinkSyncModule):
|
||||
"""Mock http requests from sync module."""
|
||||
"""Mock blink sync module object."""
|
||||
|
||||
def __init__(self, blink, header):
|
||||
"""Create mock sync module instance."""
|
||||
super().__init__(blink, header, network_id=None, camera_list=None)
|
||||
self.blink = blink
|
||||
self.header = header
|
||||
self.return_value = None
|
||||
self.return_value2 = None
|
||||
def get_network_info(self):
|
||||
"""Mock network info method."""
|
||||
return True
|
||||
|
||||
def http_get(self, url, stream=False, json=True):
|
||||
"""Mock get request."""
|
||||
if stream and self.return_value2 is not None:
|
||||
return self.return_value2
|
||||
return self.return_value
|
||||
|
||||
def http_post(self, url):
|
||||
"""Mock post request."""
|
||||
return self.return_value
|
||||
class MockCamera(BlinkCamera):
|
||||
"""Mock blink camera object."""
|
||||
|
||||
def update(self, config, force_cache=False, **kwargs):
|
||||
"""Mock camera update method."""
|
||||
|
||||
|
||||
class TestBlinkFunctions(unittest.TestCase):
|
||||
@@ -121,3 +115,16 @@ class TestBlinkFunctions(unittest.TestCase):
|
||||
with self.assertLogs() as dl_log:
|
||||
blink.download_videos("/tmp", camera="bar", stop=2)
|
||||
self.assertEqual(dl_log.output, expected_log)
|
||||
|
||||
@mock.patch("blinkpy.blinkpy.api.request_network_update")
|
||||
@mock.patch("blinkpy.auth.Auth.query")
|
||||
def test_refresh(self, mock_req, mock_update):
|
||||
"""Test ability to refresh system."""
|
||||
mock_update.return_value = {"network": {"sync_module_error": False}}
|
||||
mock_req.return_value = None
|
||||
self.blink.last_refresh = 0
|
||||
self.blink.available = True
|
||||
self.blink.sync["foo"] = MockSyncModule(self.blink, "foo", 1, [])
|
||||
self.blink.cameras = {"bar": MockCamera(self.blink.sync)}
|
||||
self.blink.sync["foo"].cameras = self.blink.cameras
|
||||
self.assertTrue(self.blink.refresh())
|
||||
|
||||
+61
-6
@@ -68,8 +68,8 @@ class TestBlinkSetup(unittest.TestCase):
|
||||
self.assertEqual(self.blink.last_refresh, None)
|
||||
with mock.patch(
|
||||
"blinkpy.sync_module.BlinkSyncModule.refresh", return_value=True
|
||||
):
|
||||
self.blink.refresh()
|
||||
), mock.patch("blinkpy.blinkpy.Blink.get_homescreen", return_value=True):
|
||||
self.blink.refresh(force=True)
|
||||
|
||||
self.assertEqual(self.blink.last_refresh, now)
|
||||
self.assertEqual(self.blink.check_if_ok_to_update(), False)
|
||||
@@ -83,8 +83,10 @@ class TestBlinkSetup(unittest.TestCase):
|
||||
self.assertEqual(self.blink.sync["tEsT"], 1234)
|
||||
|
||||
@mock.patch("blinkpy.api.request_camera_usage")
|
||||
def test_setup_cameras(self, mock_req):
|
||||
@mock.patch("blinkpy.api.request_homescreen")
|
||||
def test_setup_cameras(self, mock_home, mock_req):
|
||||
"""Check retrieval of camera information."""
|
||||
mock_home.return_value = {}
|
||||
mock_req.return_value = {
|
||||
"networks": [
|
||||
{
|
||||
@@ -195,12 +197,11 @@ class TestBlinkSetup(unittest.TestCase):
|
||||
self.assertEqual(combined["fizz"], "buzz")
|
||||
self.assertEqual(combined["bar"], "foo")
|
||||
|
||||
@mock.patch("blinkpy.api.request_homescreen")
|
||||
@mock.patch("blinkpy.blinkpy.BlinkOwl.start")
|
||||
def test_initialize_blink_minis(self, mock_start, mock_home):
|
||||
def test_initialize_blink_minis(self, mock_start):
|
||||
"""Test blink mini initialization."""
|
||||
mock_start.return_value = True
|
||||
mock_home.return_value = {
|
||||
self.blink.homescreen = {
|
||||
"owls": [
|
||||
{
|
||||
"enabled": False,
|
||||
@@ -233,6 +234,60 @@ class TestBlinkSetup(unittest.TestCase):
|
||||
self.assertEqual(self.blink.sync["foo"].name, "foo")
|
||||
self.assertEqual(self.blink.sync["bar"].name, "bar")
|
||||
|
||||
def test_blink_mini_cameras_returned(self):
|
||||
"""Test that blink mini cameras are found if attached to sync module."""
|
||||
self.blink.network_ids = ["1234"]
|
||||
self.blink.homescreen = {
|
||||
"owls": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "foo",
|
||||
"network_id": 1234,
|
||||
"onboarded": True,
|
||||
"enabled": True,
|
||||
"status": "online",
|
||||
"thumbnail": "/foo/bar",
|
||||
"serial": "abc123",
|
||||
}
|
||||
]
|
||||
}
|
||||
result = self.blink.setup_owls()
|
||||
self.assertEqual(self.blink.network_ids, ["1234"])
|
||||
self.assertEqual(
|
||||
result, [{"1234": {"name": "foo", "id": "1234", "type": "mini"}}]
|
||||
)
|
||||
|
||||
self.blink.no_owls = True
|
||||
self.blink.network_ids = []
|
||||
self.blink.get_homescreen()
|
||||
result = self.blink.setup_owls()
|
||||
self.assertEqual(self.blink.network_ids, [])
|
||||
self.assertEqual(result, [])
|
||||
|
||||
@mock.patch("blinkpy.api.request_camera_usage")
|
||||
def test_blink_mini_attached_to_sync(self, mock_usage):
|
||||
"""Test that blink mini cameras are properly attached to sync module."""
|
||||
self.blink.network_ids = ["1234"]
|
||||
self.blink.homescreen = {
|
||||
"owls": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "foo",
|
||||
"network_id": 1234,
|
||||
"onboarded": True,
|
||||
"enabled": True,
|
||||
"status": "online",
|
||||
"thumbnail": "/foo/bar",
|
||||
"serial": "abc123",
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_usage.return_value = {"networks": [{"cameras": [], "network_id": 1234}]}
|
||||
result = self.blink.setup_camera_list()
|
||||
self.assertEqual(
|
||||
result, {"1234": [{"name": "foo", "id": "1234", "type": "mini"}]}
|
||||
)
|
||||
|
||||
|
||||
class MockSync:
|
||||
"""Mock sync module class."""
|
||||
|
||||
@@ -117,10 +117,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
"WARNING:blinkpy.camera:Could not retrieve calibrated "
|
||||
"temperature."
|
||||
),
|
||||
(
|
||||
"WARNING:blinkpy.camera:Could not find thumbnail for camera new"
|
||||
"\nNoneType: None"
|
||||
),
|
||||
("WARNING:blinkpy.camera:Could not find thumbnail for camera new"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -179,3 +176,10 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
attr = camera.attributes
|
||||
for key in attr:
|
||||
self.assertEqual(attr[key], None)
|
||||
|
||||
def test_camera_stream(self, mock_resp):
|
||||
"""Test that camera stream returns correct url."""
|
||||
mock_resp.return_value = {"server": "rtsps://foo.bar"}
|
||||
mini_camera = BlinkCameraMini(self.blink.sync["test"])
|
||||
self.assertEqual(self.camera.get_liveview(), "rtsps://foo.bar")
|
||||
self.assertEqual(mini_camera.get_liveview(), "rtsps://foo.bar")
|
||||
|
||||
@@ -80,11 +80,11 @@ class TestBlinkSyncModule(unittest.TestCase):
|
||||
def test_get_camera_info_fail(self, mock_resp):
|
||||
"""Test handling of failed get camera info function."""
|
||||
mock_resp.return_value = None
|
||||
self.assertEqual(self.blink.sync["test"].get_camera_info("1"), [])
|
||||
self.assertEqual(self.blink.sync["test"].get_camera_info("1"), {})
|
||||
mock_resp.return_value = {}
|
||||
self.assertEqual(self.blink.sync["test"].get_camera_info("1"), [])
|
||||
self.assertEqual(self.blink.sync["test"].get_camera_info("1"), {})
|
||||
mock_resp.return_value = {"camera": None}
|
||||
self.assertEqual(self.blink.sync["test"].get_camera_info("1"), [])
|
||||
self.assertEqual(self.blink.sync["test"].get_camera_info("1"), {})
|
||||
|
||||
def test_get_network_info(self, mock_resp):
|
||||
"""Test network retrieval."""
|
||||
|
||||
Reference in New Issue
Block a user