Compare commits

..
71 Commits
Author SHA1 Message Date
Kevin FronczakandGitHub f4480da276 Merge pull request #180 from fronzbot/dev
0.14.0
2019-05-23 16:56:46 -04:00
Kevin FronczakandGitHub 7767d3af65 Merge pull request #182 from fronzbot/update-changes
Update CHANGES.rst
2019-05-23 16:51:54 -04:00
Kevin FronczakandGitHub 79e699ba78 Update CHANGES.rst 2019-05-23 16:42:12 -04:00
Kevin FronczakandGitHub b28d6f4eb4 Merge pull request #181 from fronzbot/version-bump
Version bump
2019-05-23 16:40:14 -04:00
Kevin FronczakandGitHub d021f48c67 Update CHANGES.rst 2019-05-23 16:15:07 -04:00
Kevin Fronczak 1e71af28dd Version bump 2019-05-23 16:06:56 -04:00
Kevin FronczakandGitHub cb5867d430 Merge pull request #179 from fronzbot/bugfix
Blink instantiation bugfix
2019-05-23 14:45:26 -04:00
Kevin Fronczak bb887936b6 Blink instantiation bugfix 2019-05-23 14:35:27 -04:00
Kevin FronczakandGitHub 720d520398 Merge pull request #175 from fronzbot/update-subdomains
Change subdomain from rest.region to rest-region
2019-05-21 23:56:25 -04:00
Kevin FronczakandGitHub 8fb1116641 Merge branch 'dev' into update-subdomains 2019-05-21 23:37:00 -04:00
Kevin FronczakandGitHub c319762152 Merge pull request #178 from fronzbot/dev-bump
dev version bump
2019-05-21 23:35:25 -04:00
Kevin Fronczak 726d8b8c65 dev version bump 2019-05-21 23:14:26 -04:00
Kevin FronczakandGitHub 683650e2c2 Merge pull request #177 from fronzbot/fix-motion-detect
Fix motion detect
2019-05-21 23:13:15 -04:00
Kevin Fronczak 50b1a35168 Update tests 2019-05-21 22:46:11 -04:00
Kevin Fronczak b88a5feddf Change default interval to one minute 2019-05-21 22:38:49 -04:00
Kevin Fronczak 93472e38de Added in motion interval to allow for looking at historic motion events.
- Good debug utility
- Helps iron out rapid motion events, or missed events due to quick
calls to refresh method
2019-05-21 22:36:46 -04:00
Kevin Fronczak 85c14ede8d Move constants into helpers/constants.py 2019-05-21 22:23:31 -04:00
Kevin Fronczak 58ce109518 Use UTC for time conversions 2019-05-21 22:09:36 -04:00
Kevin Fronczak cf65648293 Add legacy flag to be able to use old 'rest.region' subdomain 2019-05-20 21:15:08 -04:00
Kevin FronczakandGitHub d46e7ed96d Merge pull request #176 from fronzbot/slug
Slugify filenames in video download to ensure OS interoperability
2019-05-20 20:43:28 -04:00
Kevin Fronczak 00395b3825 Slugify filenames in video download to ensure OS interoperability 2019-05-20 11:04:58 -04:00
Kevin FronczakandGitHub f67f0dd6a2 Dev version bump 2019-05-20 00:00:57 -04:00
Kevin FronczakandGitHub d90cd3309f Update README.rst 2019-05-19 18:00:42 -04:00
Kevin Fronczak 130aa8382a Change subdomain from rest.region to rest-region 2019-05-18 18:23:46 -04:00
Kevin FronczakandGitHub e836a6fa58 Merge pull request #174 from fronzbot/fix-battery-voltage
Change battery percentage to state
2019-05-18 18:17:38 -04:00
Kevin Fronczak 92d89b9fe5 Change battery percentage to state 2019-05-18 12:42:01 -04:00
Kevin FronczakandGitHub c10fb432f7 Merge pull request #173 from fronzbot/reduce-throttling
Remove throttling from critical api methods
2019-05-18 12:23:25 -04:00
Kevin Fronczak 37b729b597 Remove throttling from critical api methods 2019-05-18 12:15:57 -04:00
Kevin FronczakandGitHub 749c68bb8a Merge pull request #172 from fronzbot/fix-video-api
Changed log to print
2019-05-18 11:57:46 -04:00
Kevin Fronczak dcf0ce6394 Fix lint issue 2019-05-18 11:51:58 -04:00
Kevin Fronczak 102190e61b Changed log to print 2019-05-18 11:41:17 -04:00
Kevin FronczakandGitHub a7340c97ca Merge pull request #171 from fronzbot/fix-video-api
Added changed video download endpoint
2019-05-18 11:40:23 -04:00
Kevin Fronczak 43c1162634 Added vieo endpoint key changes to motion detect logic 2019-05-18 10:54:24 -04:00
Kevin Fronczak 4db7a33ef3 Added changed video download endpoint 2019-05-18 10:48:58 -04:00
Kevin FronczakandGitHub bfdc1e47bd Dev version bump 2019-03-01 22:00:37 -05:00
Kevin FronczakandGitHub afdd73118d Merge pull request #165 from fronzbot/dev
0.13.1
2019-03-01 21:59:29 -05:00
Kevin FronczakandGitHub 5511af6244 Version bump 2019-03-01 21:51:03 -05:00
Kevin FronczakandGitHub d629d9fa3a Update CHANGES.rst 2019-03-01 21:50:36 -05:00
Kevin FronczakandGitHub 3e22d83962 Merge pull request #164 from fronzbot/throttle-hotfix
Remove throttle from network_status
2019-03-01 21:45:59 -05:00
Kevin Fronczak 57b05daad7 Remove throttle from network_status 2019-03-01 21:40:27 -05:00
Kevin FronczakandGitHub 419eb51b66 Dev version bump 2019-03-01 21:06:12 -05:00
Kevin FronczakandGitHub 0262f14a53 Merge pull request #163 from fronzbot/dev
0.13.0
2019-03-01 21:04:36 -05:00
Kevin FronczakandGitHub c4b255d3bf Version bump 2019-03-01 20:54:52 -05:00
Kevin FronczakandGitHub 13b7debb33 Update CHANGES.rst 2019-03-01 20:54:21 -05:00
Kevin FronczakandGitHub a203c4a29f Merge pull request #162 from fronzbot/new-login-api
Added ability to have multiple backup api endpoints for logging in
2019-03-01 20:34:03 -05:00
Kevin Fronczak d8bac39c52 Added ability to have multiple backup api endpoints for logging in 2019-03-01 16:03:22 -05:00
Kevin FronczakandGitHub f1c06198b2 Merge pull request #161 from fronzbot/throttle-refresh
Throttle refresh
2019-03-01 15:22:30 -05:00
Kevin Fronczak 95b45f507a Change api throttle time to 2 seconds 2019-03-01 15:11:23 -05:00
Kevin Fronczak 54bfb99d60 Added simple throttle for refresh to prevent too many force calls 2019-03-01 15:10:28 -05:00
Kevin FronczakandGitHub 9dda477e3d Merge pull request #160 from fronzbot/api-change
Api change
2019-03-01 14:55:38 -05:00
Kevin Fronczak 2ca6b9edda -_- 2019-03-01 14:25:46 -05:00
Kevin Fronczak b22764614d Upgrade pylint to 2.3.0 2019-03-01 14:08:22 -05:00
Kevin Fronczak 09c168f6e8 Remove blank line at EOF 2019-03-01 13:53:13 -05:00
Kevin Fronczak 414eabd3ae Add test for get_camera method. Downgrade travis lint to 3.5.3 2019-03-01 13:52:13 -05:00
Kevin Fronczak b536bd7eb5 Build back to 3.6, forgot dist for lint 3.7 2019-03-01 13:40:10 -05:00
Kevin Fronczak 532a8d5071 lint and build python version now 3.7 2019-03-01 13:36:19 -05:00
Kevin Fronczak 793797e3a7 Fixed issues with new endpoint 2019-03-01 13:09:03 -05:00
Kevin Fronczak 602b6160ab New method to retrieve cameras due to api change 2019-03-01 11:35:30 -05:00
Kevin FronczakandGitHub aff95ba9e9 Merge pull request #158 from fronzbot/throttle-api-calls
Added throttles to api calls
2019-02-26 10:06:49 -05:00
Kevin Fronczak 74aad2af1c Added throttles to api calls 2019-02-26 09:53:49 -05:00
Kevin FronczakandGitHub b96ad8b4e4 Merge pull request #157 from fronzbot/throttle-decorator
Adds throttle decorator
2019-02-17 20:27:15 -08:00
Kevin Fronczak a8b731fe21 Adds throttle decorator 2019-02-17 23:19:28 -05:00
Kevin FronczakandGitHub ff8f091a74 Merge pull request #154 from fronzbot/dev
0.12.1
2019-01-31 18:22:09 -08:00
Kevin Fronczak 147b6143b0 Dev version bump 2019-01-31 21:21:17 -05:00
Kevin FronczakandGitHub 2835ccf7d2 Version bump 2019-01-31 21:18:02 -05:00
Kevin FronczakandGitHub 4deb53eab8 Update CHANGES.rst 2019-01-31 21:17:30 -05:00
Kevin FronczakandGitHub 1c49b1a943 Merge pull request #153 from fronzbot/revert-logging
Remove logging improvements because they sucked
2019-01-31 18:16:10 -08:00
Kevin Fronczak 67cd4ab962 fix lint 2019-01-31 21:08:55 -05:00
Kevin Fronczak 1a2017b337 Remove logging improvements because they sucked 2019-01-31 21:02:06 -05:00
Kevin Fronczak 7775898078 Update Changes 2019-01-31 20:43:56 -05:00
Kevin FronczakandGitHub 8f0bd1224d Dev version bump 2019-01-31 20:06:30 -05:00
19 changed files with 518 additions and 240 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ sudo: required
matrix:
fast_finish: true
include:
- python: "3.6"
- python: "3.5.3"
env: TOXENV=lint
- python: "3.5.3"
env: TOXENV=py35
+65 -2
View File
@@ -3,6 +3,69 @@ Changelog
A list of changes between each release
0.14.0 (2019-05-23)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**Breaking Changes:**
- ``BlinkCamera.battery`` no longer reports a percentage, instead it returns a string representing the state of the battery.
- Previous logic for calculating percentage was incorrect
- raw battery voltage can be accessed via ``BlinkCamera.battery_voltage``
**Bug Fixes:**
- Updated video endpoint (fixes broken motion detection)
- Removed throttling from critical api methods which prevented proper operation of multi-sync unit setups
- Slugify downloaded video names to allow for OS interoperability
- Added one minute offset (``Blink.motion_interval``) when checking for recent motion to allow time for events to propagate to server prior to refresh call.
**Everything else:**
- Changed all urls to use ``rest-region`` rather than ``rest.region``. Ability to revert to old method is enabled by instantiating ``Blink()`` with the ``legacy_subdomain`` variable set to ``True``.
- Added debug mode to ``blinkpy.download_videos`` routine to simply print the videos prepped for download, rather than actually saving them.
- Use UTC for time conversions, rather than local timezone
0.13.1 (2019-03-01)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Remove throttle decorator from network status request
0.13.0 (2019-03-01)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**Breaking change:**
Wifi status reported in dBm again, instead of bars (which is great). Also, the old ``get_camera_info`` method has changed and requires a ``camera_id`` parameter.
- Adds throttle decorator
- Decorate following functions with 4s throttle (call method with ``force=True`` to override):
- request_network_status
- request_syncmodule
- request_system_arm
- request_system_disarm
- request_sync_events
- request_new_image
- request_new_video
- request_video_count
- request_cameras
- request_camera_info
- request_camera_sensors
- request_motion_detection_enable
- request_motion_detection_disable
- Use the updated homescreen api endpoint to retrieve camera information. The old method to retrieve all cameras at once seems to not exist, and this was the only solution I could figure out and confirm to work.
- Adds throttle decorator to refresh function to prevent too many frequent calls with ``force_cache`` flag set to ``True``. This additional throttle can be overridden with the ``force=True`` argument passed to the refresh function.
- Add ability to cycle through login api endpoints to anticipate future endpoint deprecation
0.12.1 (2019-01-31)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Remove logging improvements since they were incompatible with home-assistant logging
0.12.0 (2019-01-31)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Fix video api endpoint, re-enables motion detection
- Add improved logging capability
- Add download video method
- Prevent blinkpy from failing at setup due to api error
0.11.2 (2019-01-23)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Hotfix to prevent platform from stalling due to API change
@@ -29,7 +92,7 @@ A list of changes between each release
0.10.2 (2018-10-30)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Set minimum required version of the requests library to 2.20.0 due to vulnerability in earlier releases.
- When multiple networks detected, changed log level to 'warning' from 'error'
- When multiple networks detected, changed log level to ``warning`` from ``error``
0.10.1 (2018-10-18)
@@ -57,7 +120,7 @@ A list of changes between each release
- Use session for http requests
**Breaking change:**
- Cameras now accessed through sync module Blink.sync.cameras
- Cameras now accessed through sync module ``Blink.sync.cameras``
0.8.1 (2018-09-24)
+7 -17
View File
@@ -2,6 +2,10 @@ blinkpy |Build Status| |Coverage Status| |Docs| |PyPi Version| |Python Version|
================================================================================
A Python library for the Blink Camera system
Like the library? Consider buying me a cup of coffee!
|Donate|
Disclaimer:
~~~~~~~~~~~~~~~
Published under the MIT license - See LICENSE file for more details.
@@ -51,23 +55,6 @@ 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.
At initialization, you may also set the logging level of the ``blinkpy`` library like so (default is ``INFO``:
.. code:: python
import logging
from blinkpy import blinkpy
blink = blinkpy.Blink(..., loglevel=logging.<LEVEL>)
blink.start()
You can also disable logging of duplicate entries via the ``allow_duplicate_logs`` flag (default is ``True``):
.. code:: python
from blinkpy import blinkpy
blink = blinkpy.Blink(..., allow_duplicate_logs=False)
blink.start()
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:
@@ -123,3 +110,6 @@ Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34a
:target: http://blinkpy.readthedocs.io/en/latest/?badge=latest
.. |Python Version| image:: https://img.shields.io/pypi/pyversions/blinkpy.svg
:target: https://img.shields.io/pypi/pyversions/blinkpy.svg
.. |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
+19 -7
View File
@@ -3,11 +3,13 @@
import logging
from json import dumps
import blinkpy.helpers.errors as ERROR
from blinkpy.helpers.util import http_req, get_time, BlinkException
from blinkpy.helpers.util import http_req, get_time, BlinkException, Throttle
from blinkpy.helpers.constants import DEFAULT_URL
_LOGGER = logging.getLogger(__name__)
MIN_THROTTLE_TIME = 2
def request_login(blink, url, username, password, is_retry=False):
"""
@@ -60,6 +62,7 @@ def request_syncmodule(blink, network):
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_system_arm(blink, network):
"""
Arm system.
@@ -71,6 +74,7 @@ def request_system_arm(blink, network):
return http_post(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_system_disarm(blink, network):
"""
Disarm system.
@@ -96,12 +100,15 @@ def request_command_status(blink, network, command_id):
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_homescreen(blink):
"""Request homescreen info."""
url = "{}/homescreen".format(blink.urls.base_url)
url = "{}/api/v3/accounts/{}/homescreen".format(blink.urls.base_url,
blink.account_id)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_sync_events(blink, network):
"""
Request events from sync module.
@@ -113,6 +120,7 @@ def request_sync_events(blink, network):
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_new_image(blink, network, camera_id):
"""
Request to capture new thumbnail for camera.
@@ -127,6 +135,7 @@ def request_new_image(blink, network, camera_id):
return http_post(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_new_video(blink, network, camera_id):
"""
Request to capture new video clip.
@@ -141,6 +150,7 @@ def request_new_video(blink, network, camera_id):
return http_post(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_video_count(blink):
"""Request total video count."""
url = "{}/api/v2/videos/count".format(blink.urls.base_url)
@@ -156,8 +166,8 @@ def request_videos(blink, time=None, page=0):
:param page: Page number to get videos from.
"""
timestamp = get_time(time)
url = "{}/api/v2/videos/changed?since={}&page={}".format(
blink.urls.base_url, timestamp, page)
url = "{}/api/v1/accounts/{}/media/changed?since={}&page={}".format(
blink.urls.base_url, blink.account_id, timestamp, page)
return http_get(blink, url)
@@ -180,9 +190,9 @@ def request_camera_info(blink, network, camera_id):
:param network: Sync module network id.
:param camera_id: Camera ID of camera to request info from.
"""
url = "{}/network/{}/camera/{}".format(blink.urls.base_url,
network,
camera_id)
url = "{}/network/{}/camera/{}/config".format(blink.urls.base_url,
network,
camera_id)
return http_get(blink, url)
@@ -200,6 +210,7 @@ def request_camera_sensors(blink, network, camera_id):
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_motion_detection_enable(blink, network, camera_id):
"""
Enable motion detection for a camera.
@@ -214,6 +225,7 @@ def request_motion_detection_enable(blink, network, camera_id):
return http_post(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_motion_detection_disable(blink, network, camera_id):
"""Disable motion detection for a camera.
+113 -77
View File
@@ -21,29 +21,30 @@ from shutil import copyfileobj
from requests.structures import CaseInsensitiveDict
from dateutil.parser import parse
from slugify import slugify
from blinkpy import api
from blinkpy.sync_module import BlinkSyncModule
from blinkpy.helpers import errors as ERROR
from blinkpy.helpers import log
from blinkpy.helpers.util import (
create_session, merge_dicts, get_time, BlinkURLHandler,
BlinkAuthenticationException)
BlinkAuthenticationException, Throttle)
from blinkpy.helpers.constants import (
BLINK_URL, LOGIN_URL, LOGIN_BACKUP_URL)
BLINK_URL, LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL,
DEFAULT_MOTION_INTERVAL, DEFAULT_REFRESH, MIN_THROTTLE_TIME)
from blinkpy.helpers.constants import __version__
REFRESH_RATE = 30
_LOGGER = log.create_logger('blinkpy')
_LOGGER = logging.getLogger(__name__)
class Blink():
"""Class to initialize communication."""
def __init__(self, username=None, password=None,
refresh_rate=REFRESH_RATE, loglevel=logging.INFO,
allow_duplicate_logs=True):
refresh_rate=DEFAULT_REFRESH,
motion_interval=DEFAULT_MOTION_INTERVAL,
legacy_subdomain=False):
"""
Initialize Blink system.
@@ -51,9 +52,13 @@ class Blink():
:param password: Blink password
:param refresh_rate: Refresh rate of blink information.
Defaults to 15 (seconds)
:param loglevel: Sets the log level for the logger.
:param allow_duplicate_logs: Set to 'False' to only allow a log
message to be logged once.
: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._password = password
@@ -73,11 +78,9 @@ class Blink():
self.cameras = CaseInsensitiveDict({})
self.video_list = CaseInsensitiveDict({})
self._login_url = LOGIN_URL
self.motion_interval = motion_interval
self.version = __version__
self.allow_duplicate_logs = allow_duplicate_logs
self.loglevel = loglevel
self._reset_logger()
self.legacy = legacy_subdomain
@property
def auth_header(self):
@@ -91,17 +94,22 @@ class Blink():
Method logs in and sets auth token, urls, and ids for future requests.
Essentially this is just a wrapper function for ease of use.
"""
self._reset_logger()
if self._username is None or self._password is None:
if not self.login():
return
elif not self.get_auth_token():
return
camera_list = self.get_cameras()
networks = self.get_ids()
for network_name, network_id in networks.items():
sync_module = BlinkSyncModule(self, network_name, network_id)
if network_id not in camera_list.keys():
camera_list[network_id] = {}
_LOGGER.warning("No cameras found for %s", network_name)
sync_module = BlinkSyncModule(self,
network_name,
network_id,
camera_list[network_id])
sync_module.start()
self.sync[network_name] = sync_module
self.cameras = self.merge_cameras()
@@ -111,7 +119,7 @@ class Blink():
self._username = input("Username:")
self._password = getpass.getpass("Password:")
if self.get_auth_token():
_LOGGER.info("Login successful!")
_LOGGER.debug("Login successful!")
return True
_LOGGER.warning("Unable to login with %s.", self._username)
return False
@@ -123,33 +131,12 @@ class Blink():
if not isinstance(self._password, str):
raise BlinkAuthenticationException(ERROR.PASSWORD)
login_url = LOGIN_URL
response = api.request_login(self,
login_url,
self._username,
self._password,
is_retry=is_retry)
try:
if response.status_code != 200:
_LOGGER.debug("Received response code %s during login.",
response.status_code)
login_url = LOGIN_BACKUP_URL
response = api.request_login(self,
login_url,
self._username,
self._password,
is_retry=is_retry)
response = response.json()
(self.region_id, self.region), = response['region'].items()
except AttributeError:
_LOGGER.error("Login API endpoint failed with response %s",
response,
exc_info=True)
login_urls = [LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL]
response = self.login_request(login_urls, is_retry=is_retry)
if not response:
return False
except KeyError:
_LOGGER.warning("Could not extract region info.")
self.region_id = 'piri'
self.region = 'UNKNOWN'
self._host = "{}.{}".format(self.region_id, BLINK_URL)
self._token = response['authtoken']['authtoken']
@@ -157,11 +144,45 @@ class Blink():
self._auth_header = {'Host': self._host,
'TOKEN_AUTH': self._token}
self.urls = BlinkURLHandler(self.region_id)
self._login_url = login_url
self.urls = BlinkURLHandler(self.region_id, legacy=self.legacy)
return self._auth_header
def login_request(self, login_urls, is_retry=False):
"""Make a login request."""
try:
login_url = login_urls.pop(0)
except IndexError:
_LOGGER.error("Could not login to blink servers.")
return False
_LOGGER.info("Attempting login with %s", login_url)
response = api.request_login(self,
login_url,
self._username,
self._password,
is_retry=is_retry)
try:
if response.status_code != 200:
response = self.login_request(login_urls)
response = response.json()
(self.region_id, self.region), = response['region'].items()
except AttributeError:
_LOGGER.error("Login API endpoint failed with response %s",
response,
exc_info=True)
return False
except KeyError:
_LOGGER.warning("Could not extract region info.")
self.region_id = 'piri'
self.region = 'UNKNOWN'
self._login_url = login_url
return response
def get_ids(self):
"""Set the network ID and Account ID."""
response = api.request_networks(self)
@@ -181,6 +202,26 @@ class Blink():
self.network_ids = all_networks
return network_dict
def get_cameras(self):
"""Retrieve a camera list for each onboarded network."""
response = api.request_homescreen(self)
try:
all_cameras = {}
for camera in response['cameras']:
camera_network = str(camera['network_id'])
camera_name = camera['name']
camera_id = camera['id']
camera_info = {'name': camera_name, 'id': camera_id}
if camera_network not in all_cameras:
all_cameras[camera_network] = []
all_cameras[camera_network].append(camera_info)
return all_cameras
except KeyError:
_LOGGER.error("Initialization failue. Could not retrieve cameras.")
return {}
@Throttle(seconds=MIN_THROTTLE_TIME)
def refresh(self, force_cache=False):
"""
Perform a system refresh.
@@ -194,6 +235,8 @@ class Blink():
if not force_cache:
# Prevents rapid clearing of motion detect property
self.last_refresh = int(time.time())
return True
return False
def check_if_ok_to_update(self):
"""Check if it is ok to perform an http request."""
@@ -212,7 +255,8 @@ class Blink():
combined = merge_dicts(combined, self.sync[sync].cameras)
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.
@@ -223,6 +267,8 @@ class Blink():
:param camera: Camera name to retrieve. Defaults to "all".
Use a list for multiple cameras.
:param stop: Page to stop on (~25 items per page. Default page 10).
:param debug: Set to TRUE to prevent downloading of items.
Instead of downloading, entries will be printed to log.
"""
if since is None:
since_epochs = self.last_refresh
@@ -240,23 +286,23 @@ class Blink():
response = api.request_videos(self, time=since_epochs, page=page)
_LOGGER.debug("Processing page %s", page)
try:
result = response['videos']
result = response['media']
if not result:
raise IndexError
except (KeyError, IndexError):
_LOGGER.info("No videos found on page %s. Exiting.", page)
break
self._parse_downloaded_items(result, camera, path)
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."""
for item in result:
try:
created_at = item['created_at']
camera_name = item['camera_name']
camera_name = item['device_name']
is_deleted = item['deleted']
address = item['address']
address = item['media']
except KeyError:
_LOGGER.info("Missing clip information, skipping...")
continue
@@ -272,32 +318,22 @@ class Blink():
continue
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)
if os.path.isfile(filename):
_LOGGER.info("%s already exists, skipping...", filename)
continue
if not debug:
if os.path.isfile(filename):
_LOGGER.info("%s already exists, skipping...", filename)
continue
response = api.http_get(self, url=clip_address,
stream=True, json=False)
with open(filename, 'wb') as vidfile:
copyfileobj(response.raw, vidfile)
response = api.http_get(self, url=clip_address,
stream=True, json=False)
with open(filename, 'wb') as vidfile:
copyfileobj(response.raw, vidfile)
_LOGGER.info("Downloaded video to %s", filename)
# pylint: disable=no-self-use
def _reset_logger(self):
"""Reset the log handler."""
for handler in _LOGGER.handlers:
_LOGGER.removeHandler(handler)
handler.close()
_LOGGER.setLevel(self.loglevel)
if self.allow_duplicate_logs:
handler = logging.StreamHandler()
handler.setFormatter(log.log_formatter())
else:
handler = log.RepeatLogHandler()
handler.setFormatter(log.log_formatter())
_LOGGER.addHandler(handler)
_LOGGER.info("Downloaded video to %s", filename)
else:
print(("Camera: {}, Timestamp: {}, "
"Address: {}, Filename: {}").format(
camera_name, created_at, address, filename))
+6 -4
View File
@@ -41,6 +41,7 @@ class BlinkCamera():
'temperature_c': self.temperature_c,
'temperature_calibrated': self.temperature_calibrated,
'battery': self.battery,
'battery_voltage': self.battery_voltage,
'thumbnail': self.thumbnail,
'video': self.clip,
'motion_enabled': self.motion_enabled,
@@ -54,8 +55,8 @@ class BlinkCamera():
@property
def battery(self):
"""Return battery level as percentage."""
return round(self.battery_voltage / 180 * 100)
"""Return battery as string."""
return self.battery_state
@property
def temperature_c(self):
@@ -92,10 +93,11 @@ class BlinkCamera():
self.network_id,
self.camera_id)
def update(self, config, force_cache=False):
def update(self, config, force_cache=False, **kwargs):
"""Update camera info."""
# force = kwargs.pop('force', False)
self.name = config['name']
self.camera_id = str(config['camera_id'])
self.camera_id = str(config['id'])
self.network_id = str(config['network_id'])
self.serial = config['serial']
self.motion_enabled = config['enabled']
+7 -2
View File
@@ -3,7 +3,7 @@
import os
MAJOR_VERSION = 0
MINOR_VERSION = 12
MINOR_VERSION = 14
PATCH_VERSION = 0
__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
@@ -47,7 +47,8 @@ URLS
BLINK_URL = 'immedia-semi.com'
DEFAULT_URL = "{}.{}".format('prod', BLINK_URL)
BASE_URL = "https://{}".format(DEFAULT_URL)
LOGIN_URL = "{}/login".format(BASE_URL)
LOGIN_URL = "{}/api/v2/login".format(BASE_URL)
OLD_LOGIN_URL = "{}/login".format(BASE_URL)
LOGIN_BACKUP_URL = "https://{}.{}/login".format('rest.piri', BLINK_URL)
'''
@@ -59,3 +60,7 @@ ONLINE = {'online': True, 'offline': False}
OTHER
'''
TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S%Z'
DEFAULT_MOTION_INTERVAL = 1
DEFAULT_REFRESH = 30
MIN_THROTTLE_TIME = 2
-33
View File
@@ -1,33 +0,0 @@
"""Module for blinkpy logging."""
import logging
def create_logger(name):
"""Create a logger instance."""
handler = RepeatLogHandler()
handler.setFormatter(log_formatter())
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
logger.addHandler(handler)
return logger
def log_formatter():
"""Create log formatter."""
fmt = "%(asctime)s %(levelname)s [%(name)s] %(message)s"
return logging.Formatter(fmt)
class RepeatLogHandler(logging.StreamHandler):
"""Log handler for repeat entries."""
def __init__(self):
"""Initialize repeat log handler."""
super().__init__()
self.log_record = set()
def emit(self, record):
"""Ensure we only log a message once."""
if record.msg not in self.log_record:
self.log_record.add(record.msg)
super().emit(record)
+37 -5
View File
@@ -2,6 +2,7 @@
import logging
import time
from functools import wraps
from requests import Request, Session, exceptions
from blinkpy.helpers.constants import BLINK_URL, TIMESTAMP_FORMAT
import blinkpy.helpers.errors as ERROR
@@ -14,7 +15,7 @@ def get_time(time_to_convert=None):
"""Create blink-compatible timestamp."""
if time_to_convert is None:
time_to_convert = time.time()
return time.strftime(TIMESTAMP_FORMAT, time.localtime(time_to_convert))
return time.strftime(TIMESTAMP_FORMAT, time.gmtime(time_to_convert))
def merge_dicts(dict_a, dict_b):
@@ -106,18 +107,49 @@ class BlinkException(Exception):
class BlinkAuthenticationException(BlinkException):
"""Class to throw authentication exception."""
pass
class BlinkURLHandler():
"""Class that handles Blink URLS."""
def __init__(self, region_id):
def __init__(self, region_id, legacy=False):
"""Initialize the urls."""
self.base_url = "https://rest.{}.{}".format(region_id, BLINK_URL)
self.subdomain = 'rest-{}'.format(region_id)
if legacy:
self.subdomain = 'rest.{}'.format(region_id)
self.base_url = "https://{}.{}".format(self.subdomain, BLINK_URL)
self.home_url = "{}/homescreen".format(self.base_url)
self.event_url = "{}/events/network".format(self.base_url)
self.network_url = "{}/network".format(self.base_url)
self.networks_url = "{}/networks".format(self.base_url)
self.video_url = "{}/api/v2/videos".format(self.base_url)
_LOGGER.debug("Setting base url to %s.", self.base_url)
class Throttle():
"""Class for throttling api calls."""
def __init__(self, seconds=10):
"""Initialize throttle class."""
self.throttle_time = seconds
self.last_call = 0
def __call__(self, method):
"""Throttle caller method."""
def throttle_method():
"""Call when method is throttled."""
return None
@wraps(method)
def wrapper(*args, **kwargs):
"""Wrap that checks for throttling."""
force = kwargs.pop('force', False)
now = int(time.time())
last_call_delta = now - self.last_call
if force or last_call_delta > self.throttle_time:
result = method(*args, *kwargs)
self.last_call = now
return result
return throttle_method()
return wrapper
+52 -28
View File
@@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__)
class BlinkSyncModule():
"""Class to initialize sync module."""
def __init__(self, blink, network_name, network_id):
def __init__(self, blink, network_name, network_id, camera_list):
"""
Initialize Blink sync module.
@@ -30,12 +30,13 @@ class BlinkSyncModule():
self.sync_id = None
self.host = None
self.summary = None
self.homescreen = None
self.network_info = None
self.events = []
self.cameras = CaseInsensitiveDict({})
self.motion_interval = blink.motion_interval
self.motion = {}
self.last_record = {}
self.camera_list = camera_list
@property
def attributes(self):
@@ -64,7 +65,10 @@ class BlinkSyncModule():
@property
def arm(self):
"""Return status of sync module: armed/disarmed."""
return self.network_info['network']['armed']
try:
return self.network_info['network']['armed']
except (KeyError, TypeError):
return None
@arm.setter
def arm(self, value):
@@ -76,7 +80,8 @@ class BlinkSyncModule():
def start(self):
"""Initialize the system."""
response = api.request_syncmodule(self.blink, self.network_id)
response = api.request_syncmodule(self.blink,
self.network_id)
try:
self.summary = response['syncmodule']
self.network_id = self.summary['network_id']
@@ -94,24 +99,35 @@ class BlinkSyncModule():
response,
exc_info=True)
self.events = self.get_events()
self.homescreen = api.request_homescreen(self.blink)
self.network_info = api.request_network_status(self.blink,
self.network_id)
self.check_new_videos()
camera_info = self.get_camera_info()
for camera_config in camera_info:
name = camera_config['name']
self.cameras[name] = BlinkCamera(self)
self.motion[name] = False
self.cameras[name].update(camera_config, force_cache=True)
try:
for camera_config in self.camera_list:
if 'name' not in camera_config:
break
name = camera_config['name']
self.cameras[name] = BlinkCamera(self)
self.motion[name] = False
camera_info = self.get_camera_info(camera_config['id'])
self.cameras[name].update(camera_info,
force_cache=True,
force=True)
except KeyError:
_LOGGER.error("Could not create cameras instances for %s",
self.name,
exc_info=True)
return False
return True
def get_events(self):
def get_events(self, **kwargs):
"""Retrieve events from server."""
response = api.request_sync_events(self.blink, self.network_id)
force = kwargs.pop('force', False)
response = api.request_sync_events(self.blink,
self.network_id,
force=force)
try:
return response['event']
except (TypeError, KeyError):
@@ -120,11 +136,13 @@ class BlinkSyncModule():
exc_info=True)
return False
def get_camera_info(self):
def get_camera_info(self, camera_id):
"""Retrieve camera information."""
response = api.request_cameras(self.blink, self.network_id)
response = api.request_camera_info(self.blink,
self.network_id,
camera_id)
try:
return response['devicestatus']
return response['camera'][0]
except (TypeError, KeyError):
_LOGGER.error("Could not extract camera info: %s",
response,
@@ -133,35 +151,41 @@ class BlinkSyncModule():
def refresh(self, force_cache=False):
"""Get all blink cameras and pulls their most recent status."""
self.events = self.get_events()
self.homescreen = api.request_homescreen(self.blink)
self.network_info = api.request_network_status(self.blink,
self.network_id)
camera_info = self.get_camera_info()
self.check_new_videos()
for camera_config in camera_info:
name = camera_config['name']
self.cameras[name].update(camera_config, force_cache=force_cache)
for camera_name in self.cameras.keys():
camera_id = self.cameras[camera_name].camera_id
camera_info = self.get_camera_info(camera_id)
self.cameras[camera_name].update(camera_info,
force_cache=force_cache)
def check_new_videos(self):
"""Check if new videos since last refresh."""
try:
interval = self.blink.last_refresh - self.motion_interval*60
except TypeError:
# This is the first start, so refresh hasn't happened yet.
# No need to check for motion.
return False
resp = api.request_videos(self.blink,
time=self.blink.last_refresh,
page=0)
time=interval,
page=1)
for camera in self.cameras.keys():
self.motion[camera] = False
try:
info = resp['videos']
info = resp['media']
except (KeyError, TypeError):
_LOGGER.warning("Could not check for motion. Response: %s", resp)
return False
for entry in info:
try:
name = entry['camera_name']
clip = entry['address']
name = entry['device_name']
clip = entry['media']
timestamp = entry['created_at']
self.motion[name] = True
self.last_record[name] = {'clip': clip, 'time': timestamp}
+4
View File
@@ -8,6 +8,8 @@ reports=no
# too-many-* - are not enforced for the sake of readability
# too-few-* - same as too-many-*
# no-else-return - I don't see any reason to enforce this. both forms are readable
# no-self-use - stupid and only annoying
# unexpected-keyword-arg - doesn't allow for use of **kwargs, which is dumb
disable=
locally-disabled,
@@ -23,3 +25,5 @@ disable=
too-many-lines,
too-few-public-methods,
no-else-return,
no-self-use,
unexpected-keyword-arg,
+1
View File
@@ -1,3 +1,4 @@
python-dateutil==2.7.5
requests>=2.20.0
python-slugify==3.0.2
testtools==2.3.0
+1 -1
View File
@@ -1,6 +1,6 @@
flake8==3.5.0
flake8-docstrings==1.3.0
pylint==2.1.1
pylint==2.3.0
pydocstyle==2.1.1
pytest==3.7.1
pytest-cov>=2.3.1
-2
View File
@@ -67,5 +67,3 @@ def mocked_session_send(*args, **kwargs):
class MockURLHandler(BlinkURLHandler):
"""Mocks URL Handler in blinkpy module."""
pass
+44 -28
View File
@@ -2,7 +2,6 @@
import unittest
from unittest import mock
import logging
from requests import Request
from blinkpy import blinkpy
from blinkpy.sync_module import BlinkSyncModule
@@ -56,31 +55,39 @@ class TestBlinkFunctions(unittest.TestCase):
"""Clean up after test."""
self.blink = None
@mock.patch('blinkpy.blinkpy.api.http_req')
@mock.patch('blinkpy.blinkpy.api.request_login')
def test_backup_url(self, req, mock_sess):
"""Test backup login method."""
fake_req = Request('POST', 'http://wrong.url').prepare()
json_resp = {
'authtoken': {'authtoken': 'foobar123'},
'networks': {'1234': {'name': 'foobar', 'onboarded': True}}
}
bad_req = mresp.MockResponse({}, 404)
new_req = mresp.MockResponse(json_resp, 200)
req.side_effect = [
mresp.mocked_session_send(fake_req),
bad_req,
bad_req,
new_req
]
self.blink.get_auth_token()
self.assertEqual(self.blink.region_id, 'piri')
self.assertEqual(self.blink.region, 'UNKNOWN')
self.blink.login_request(['test1', 'test2', 'test3'])
# pylint: disable=protected-access
self.assertEqual(self.blink._token, 'foobar123')
self.assertEqual(self.blink._login_url, 'test3')
req.side_effect = [
bad_req,
new_req,
bad_req
]
self.blink.login_request(['test1', 'test2', 'test3'])
# pylint: disable=protected-access
self.assertEqual(self.blink._login_url, 'test2')
def test_merge_cameras(self, mock_sess):
"""Test merge camera functionality."""
first_dict = {'foo': 'bar', 'test': 123}
next_dict = {'foobar': 456, 'bar': 'foo'}
self.blink.sync['foo'] = BlinkSyncModule(self.blink, 'foo', 1)
self.blink.sync['bar'] = BlinkSyncModule(self.blink, 'bar', 2)
self.blink.sync['foo'] = BlinkSyncModule(self.blink, 'foo', 1, [])
self.blink.sync['bar'] = BlinkSyncModule(self.blink, 'bar', 2, [])
self.blink.sync['foo'].cameras = first_dict
self.blink.sync['bar'].cameras = next_dict
result = self.blink.merge_cameras()
@@ -90,14 +97,17 @@ class TestBlinkFunctions(unittest.TestCase):
@mock.patch('blinkpy.blinkpy.api.request_videos')
def test_download_video_exit(self, mock_req, mock_sess):
"""Test we exit method when provided bad response."""
blink = blinkpy.Blink(loglevel=logging.DEBUG)
blink = blinkpy.Blink()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
blink.last_refresh = 0
mock_req.return_value = {}
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy:Retrieving videos since {}".format(formatted_date),
"DEBUG:blinkpy:Processing page 1",
"INFO:blinkpy:No videos found on page 1. Exiting."
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"INFO:blinkpy.blinkpy:No videos found on page 1. Exiting."
]
with self.assertLogs() as dl_log:
blink.download_videos('/tmp')
@@ -106,21 +116,24 @@ class TestBlinkFunctions(unittest.TestCase):
@mock.patch('blinkpy.blinkpy.api.request_videos')
def test_parse_downloaded_items(self, mock_req, mock_sess):
"""Test ability to parse downloaded items list."""
blink = blinkpy.Blink(loglevel=logging.DEBUG)
blink = blinkpy.Blink()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
generic_entry = {
'created_at': '1970',
'camera_name': 'foo',
'device_name': 'foo',
'deleted': True,
'address': '/bar.mp4'
'media': '/bar.mp4'
}
result = [generic_entry]
mock_req.return_value = {'videos': result}
mock_req.return_value = {'media': result}
blink.last_refresh = 0
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy:Retrieving videos since {}".format(formatted_date),
"DEBUG:blinkpy:Processing page 1",
"DEBUG:blinkpy:foo: /bar.mp4 is marked as deleted."
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"DEBUG:blinkpy.blinkpy:foo: /bar.mp4 is marked as deleted."
]
with self.assertLogs() as dl_log:
blink.download_videos('/tmp', stop=2)
@@ -129,21 +142,24 @@ class TestBlinkFunctions(unittest.TestCase):
@mock.patch('blinkpy.blinkpy.api.request_videos')
def test_parse_camera_not_in_list(self, mock_req, mock_sess):
"""Test ability to parse downloaded items list."""
blink = blinkpy.Blink(loglevel=logging.DEBUG)
blink = blinkpy.Blink()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
generic_entry = {
'created_at': '1970',
'camera_name': 'foo',
'device_name': 'foo',
'deleted': True,
'address': '/bar.mp4'
'media': '/bar.mp4'
}
result = [generic_entry]
mock_req.return_value = {'videos': result}
mock_req.return_value = {'media': result}
blink.last_refresh = 0
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy:Retrieving videos since {}".format(formatted_date),
"DEBUG:blinkpy:Processing page 1",
"DEBUG:blinkpy:Skipping videos for foo."
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"DEBUG:blinkpy.blinkpy:Skipping videos for foo."
]
with self.assertLogs() as dl_log:
blink.download_videos('/tmp', camera='bar', stop=2)
+24 -1
View File
@@ -31,7 +31,10 @@ class TestBlinkSetup(unittest.TestCase):
self.blink_no_cred = Blink()
self.blink = Blink(username=USERNAME,
password=PASSWORD)
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', '1234')
self.blink.sync['test'] = BlinkSyncModule(self.blink,
'test',
'1234',
[])
self.blink.urls = BlinkURLHandler('test')
self.blink.session = create_session()
@@ -160,3 +163,23 @@ class TestBlinkSetup(unittest.TestCase):
"""Check that we appropriately handle unexpected login info."""
mock_login.return_value = None
self.assertFalse(self.blink.get_auth_token())
@mock.patch('blinkpy.api.request_homescreen')
def test_get_cameras(self, mock_home, mock_sess):
"""Check retrieval of camera information."""
mock_home.return_value = {
'cameras': [{'name': 'foo', 'network_id': 1234, 'id': 5678},
{'name': 'bar', 'network_id': 1234, 'id': 5679},
{'name': 'test', 'network_id': 4321, 'id': 0000}]
}
result = self.blink.get_cameras()
self.assertEqual(result, {'1234': [{'name': 'foo', 'id': 5678},
{'name': 'bar', 'id': 5679}],
'4321': [{'name': 'test', 'id': 0000}]})
@mock.patch('blinkpy.api.request_homescreen')
def test_get_cameras_failure(self, mock_home, mock_sess):
"""Check that on failure we initialize empty info and move on."""
mock_home.return_value = {}
result = self.blink.get_cameras()
self.assertEqual(result, {})
+10 -10
View File
@@ -46,7 +46,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
self.blink._auth_header = header
self.blink.session = create_session()
self.blink.urls = BlinkURLHandler('test')
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', 1234)
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', 1234, [])
self.camera = BlinkCamera(self.blink.sync['test'])
self.camera.name = 'foobar'
self.blink.sync['test'].cameras['foobar'] = self.camera
@@ -59,7 +59,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
"""Test that we can properly update camera properties."""
config = {
'name': 'new',
'camera_id': 1234,
'id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
@@ -87,15 +87,15 @@ class TestBlinkCameraSetup(unittest.TestCase):
self.assertEqual(self.camera.network_id, '5678')
self.assertEqual(self.camera.serial, '12345678')
self.assertEqual(self.camera.motion_enabled, False)
self.assertEqual(self.camera.battery, 50)
self.assertEqual(self.camera.battery, 'ok')
self.assertEqual(self.camera.temperature, 68)
self.assertEqual(self.camera.temperature_c, 20)
self.assertEqual(self.camera.temperature_calibrated, 71)
self.assertEqual(self.camera.wifi_strength, 4)
self.assertEqual(self.camera.thumbnail,
'https://rest.test.immedia-semi.com/thumb.jpg')
'https://rest-test.immedia-semi.com/thumb.jpg')
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.video_from_cache, 'foobar')
@@ -115,7 +115,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
}
config = {
'name': 'new',
'camera_id': 1234,
'id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
@@ -136,7 +136,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
}
self.camera.update(config)
self.assertEqual(self.camera.thumbnail,
'https://rest.test.immedia-semi.com/new/thumb.jpg')
'https://rest-test.immedia-semi.com/new/thumb.jpg')
def test_no_thumbnails(self, mock_sess):
"""Tests that thumbnail is 'None' if none found."""
@@ -144,7 +144,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
self.camera.last_record = ['1']
config = {
'name': 'new',
'camera_id': 1234,
'id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
@@ -159,7 +159,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
}
self.assertEqual(self.camera.temperature_calibrated, None)
with self.assertLogs() as logrecord:
self.camera.update(config)
self.camera.update(config, force=True)
self.assertEqual(self.camera.thumbnail, None)
self.assertEqual(self.camera.last_record, ['1'])
self.assertEqual(self.camera.temperature_calibrated, 68)
@@ -176,7 +176,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
mock_sess.return_value = 'foobar'
config = {
'name': 'new',
'camera_id': 1234,
'id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
+20 -22
View File
@@ -17,14 +17,19 @@ class TestBlinkSyncModule(unittest.TestCase):
def setUp(self):
"""Set up Blink module."""
self.blink = blinkpy.Blink(username=USERNAME,
password=PASSWORD)
password=PASSWORD,
motion_interval=0)
# pylint: disable=protected-access
self.blink._auth_header = {
'Host': 'test.url.tld',
'TOKEN_AUTH': 'foobar123'
}
self.blink.last_refresh = 0
self.blink.urls = blinkpy.BlinkURLHandler('test')
self.blink.sync['test'] = BlinkSyncModule(self.blink, 'test', '1234')
self.blink.sync['test'] = BlinkSyncModule(self.blink,
'test',
'1234',
[])
self.camera = BlinkCamera(self.blink.sync)
self.mock_start = [
{'syncmodule': {
@@ -52,15 +57,22 @@ class TestBlinkSyncModule(unittest.TestCase):
def test_get_camera_info(self, mock_resp):
"""Test get camera info function."""
mock_resp.return_value = {'devicestatus': True}
self.assertEqual(self.blink.sync['test'].get_camera_info(), True)
mock_resp.return_value = {'camera': ['foobar']}
self.assertEqual(self.blink.sync['test'].get_camera_info('1234'),
'foobar')
def test_check_new_videos_startup(self, mock_resp):
"""Test that check_new_videos does not block startup."""
sync_module = self.blink.sync['test']
self.blink.last_refresh = None
self.assertFalse(sync_module.check_new_videos())
def test_check_new_videos(self, mock_resp):
"""Test recent video response."""
mock_resp.return_value = {
'videos': [{
'camera_name': 'foo',
'address': '/foo/bar.mp4',
'media': [{
'device_name': 'foo',
'media': '/foo/bar.mp4',
'created_at': '1970-01-01T00:00:00+0:00'
}]
}
@@ -72,7 +84,7 @@ class TestBlinkSyncModule(unittest.TestCase):
{'clip': '/foo/bar.mp4',
'time': '1970-01-01T00:00:00+0:00'})
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.assertEqual(sync_module.motion, {'foo': False})
self.assertEqual(sync_module.last_record['foo'],
@@ -126,20 +138,6 @@ class TestBlinkSyncModule(unittest.TestCase):
self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].network_id, 8675309)
def test_unexpected_events(self, mock_resp):
"""Test unexpected events response."""
self.mock_start[1] = None
mock_resp.side_effect = self.mock_start
self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].events, False)
def test_missing_events(self, mock_resp):
"""Test missing events key from response."""
self.mock_start[1] = {}
mock_resp.side_effect = self.mock_start
self.blink.sync['test'].start()
self.assertEqual(self.blink.sync['test'].events, False)
def test_unexpected_camera_info(self, mock_resp):
"""Test unexpected camera info response."""
self.blink.sync['test'].cameras['foo'] = None
+107
View File
@@ -0,0 +1,107 @@
"""Test various api functions."""
import unittest
from unittest import mock
import time
from blinkpy.helpers.util import Throttle, BlinkURLHandler
class TestUtil(unittest.TestCase):
"""Test the helpers/util module."""
def setUp(self):
"""Initialize the blink module."""
def tearDown(self):
"""Tear down blink module."""
def test_throttle(self):
"""Test the throttle decorator."""
calls = []
@Throttle(seconds=5)
def test_throttle():
calls.append(1)
now = int(time.time())
now_plus_four = now + 4
now_plus_six = now + 6
test_throttle()
self.assertEqual(1, len(calls))
# Call again, still shouldn't fire
test_throttle()
self.assertEqual(1, len(calls))
# Call with force
test_throttle(force=True)
self.assertEqual(2, len(calls))
# Call without throttle, shouldn't fire
test_throttle()
self.assertEqual(2, len(calls))
# Fake time as 4 seconds from now
with mock.patch('time.time', return_value=now_plus_four):
test_throttle()
self.assertEqual(2, len(calls))
# Fake time as 6 seconds from now
with mock.patch('time.time', return_value=now_plus_six):
test_throttle()
self.assertEqual(3, len(calls))
def test_throttle_per_instance(self):
"""Test that throttle is done once per instance of class."""
class Tester:
"""A tester class for throttling."""
def test(self):
"""Test the throttle."""
return True
tester = Tester()
throttled = Throttle(seconds=1)(tester.test)
self.assertEqual(throttled(), True)
self.assertEqual(throttled(), None)
def test_throttle_on_two_methods(self):
"""Test that throttle works for multiple methods."""
class Tester:
"""A tester class for throttling."""
@Throttle(seconds=3)
def test1(self):
"""Test function for throttle."""
return True
@Throttle(seconds=5)
def test2(self):
"""Test function for throttle."""
return True
tester = Tester()
now = time.time()
now_plus_4 = now + 4
now_plus_6 = now + 6
self.assertEqual(tester.test1(), True)
self.assertEqual(tester.test2(), True)
self.assertEqual(tester.test1(), None)
self.assertEqual(tester.test2(), None)
with mock.patch('time.time', return_value=now_plus_4):
self.assertEqual(tester.test1(), True)
self.assertEqual(tester.test2(), None)
with mock.patch('time.time', return_value=now_plus_6):
self.assertEqual(tester.test1(), None)
self.assertEqual(tester.test2(), True)
def test_legacy_subdomains(self):
"""Test that subdomain can be set to legacy mode."""
urls = BlinkURLHandler('test')
self.assertEqual(urls.subdomain, 'rest-test')
urls = BlinkURLHandler('test', legacy=True)
self.assertEqual(urls.subdomain, 'rest.test')