Compare commits

..
8 Commits
Author SHA1 Message Date
Kevin FronczakandGitHub 77e200f6a5 Merge pull request #152 from fronzbot/dev
0.12.0
2019-01-31 17:05:03 -08:00
Kevin FronczakandGitHub f3a8036649 Merge pull request #141 from fronzbot/dev
0.11.2
2019-01-23 08:05:05 -08:00
Kevin FronczakandGitHub 853ccbce79 Merge pull request #136 from fronzbot/dev
0.11.1
2019-01-02 09:19:08 -08:00
Kevin FronczakandGitHub 1a24e1896f Merge pull request #129 from fronzbot/dev
0.11.0
2018-11-23 20:27:40 -05:00
Kevin FronczakandGitHub ae946dbb53 Merge pull request #121 from fronzbot/dev
0.10.3
2018-11-20 21:47:42 -05:00
Kevin FronczakandGitHub 80c6444c2e Merge pull request #112 from fronzbot/dev
0.10.2
2018-10-30 20:22:08 -04:00
Kevin FronczakandGitHub e872fc9ce5 Merge pull request #105 from fronzbot/dev
0.10.1
2018-10-18 13:19:49 -04:00
Kevin FronczakandGitHub 4e6957a772 Merge pull request #100 from fronzbot/dev
Release 0.10.0
2018-10-16 17:43:21 -04:00
18 changed files with 225 additions and 432 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ sudo: required
matrix:
fast_finish: true
include:
- python: "3.5.3"
- python: "3.6"
env: TOXENV=lint
- python: "3.5.3"
env: TOXENV=py35
-41
View File
@@ -3,47 +3,6 @@ Changelog
A list of changes between each release
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
+17
View File
@@ -51,6 +51,23 @@ The simplest way to use this package from a terminal is to call ``Blink.start()`
If you would like to log in without setting up the cameras or system, you can simply call the ``Blink.login()`` function which will prompt for a username and password and then authenticate with the server. This is useful if you want to avoid use of the ``start()`` function which simply acts as a wrapper for more targeted API methods.
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:
+7 -19
View File
@@ -3,13 +3,11 @@
import logging
from json import dumps
import blinkpy.helpers.errors as ERROR
from blinkpy.helpers.util import http_req, get_time, BlinkException, Throttle
from blinkpy.helpers.util import http_req, get_time, BlinkException
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):
"""
@@ -62,7 +60,6 @@ def request_syncmodule(blink, network):
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_system_arm(blink, network):
"""
Arm system.
@@ -74,7 +71,6 @@ def request_system_arm(blink, network):
return http_post(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_system_disarm(blink, network):
"""
Disarm system.
@@ -100,15 +96,12 @@ 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 = "{}/api/v3/accounts/{}/homescreen".format(blink.urls.base_url,
blink.account_id)
url = "{}/homescreen".format(blink.urls.base_url)
return http_get(blink, url)
@Throttle(seconds=MIN_THROTTLE_TIME)
def request_sync_events(blink, network):
"""
Request events from sync module.
@@ -120,7 +113,6 @@ 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.
@@ -135,7 +127,6 @@ 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.
@@ -150,7 +141,6 @@ 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)
@@ -166,8 +156,8 @@ def request_videos(blink, time=None, page=0):
:param page: Page number to get videos from.
"""
timestamp = get_time(time)
url = "{}/api/v1/accounts/{}/media/changed?since={}&page={}".format(
blink.urls.base_url, blink.account_id, timestamp, page)
url = "{}/api/v2/videos/changed?since={}&page={}".format(
blink.urls.base_url, timestamp, page)
return http_get(blink, url)
@@ -190,9 +180,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/{}/config".format(blink.urls.base_url,
network,
camera_id)
url = "{}/network/{}/camera/{}".format(blink.urls.base_url,
network,
camera_id)
return http_get(blink, url)
@@ -210,7 +200,6 @@ 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.
@@ -225,7 +214,6 @@ 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.
+74 -100
View File
@@ -25,27 +25,25 @@ from dateutil.parser import parse
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, Throttle)
BlinkAuthenticationException)
from blinkpy.helpers.constants import (
BLINK_URL, LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL)
BLINK_URL, LOGIN_URL, LOGIN_BACKUP_URL)
from blinkpy.helpers.constants import __version__
REFRESH_RATE = 30
# Prevents rapid calls to blink.refresh()
# with the force_cache flag set to True
MIN_THROTTLE_TIME = 2
_LOGGER = logging.getLogger(__name__)
_LOGGER = log.create_logger('blinkpy')
class Blink():
"""Class to initialize communication."""
def __init__(self, username=None, password=None,
refresh_rate=REFRESH_RATE):
refresh_rate=REFRESH_RATE, loglevel=logging.INFO,
allow_duplicate_logs=True):
"""
Initialize Blink system.
@@ -53,6 +51,9 @@ 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.
"""
self._username = username
self._password = password
@@ -73,6 +74,10 @@ class Blink():
self.video_list = CaseInsensitiveDict({})
self._login_url = LOGIN_URL
self.version = __version__
self.allow_duplicate_logs = allow_duplicate_logs
self.loglevel = loglevel
self._reset_logger()
@property
def auth_header(self):
@@ -86,22 +91,17 @@ 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():
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 = BlinkSyncModule(self, network_name, network_id)
sync_module.start()
self.sync[network_name] = sync_module
self.cameras = self.merge_cameras()
@@ -111,7 +111,7 @@ class Blink():
self._username = input("Username:")
self._password = getpass.getpass("Password:")
if self.get_auth_token():
_LOGGER.debug("Login successful!")
_LOGGER.info("Login successful!")
return True
_LOGGER.warning("Unable to login with %s.", self._username)
return False
@@ -123,12 +123,33 @@ class Blink():
if not isinstance(self._password, str):
raise BlinkAuthenticationException(ERROR.PASSWORD)
login_urls = [LOGIN_URL, OLD_LOGIN_URL, LOGIN_BACKUP_URL]
response = self.login_request(login_urls, is_retry=is_retry)
if not response:
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)
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']
@@ -137,44 +158,10 @@ class Blink():
self._auth_header = {'Host': self._host,
'TOKEN_AUTH': self._token}
self.urls = BlinkURLHandler(self.region_id)
self._login_url = login_url
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)
@@ -194,26 +181,6 @@ 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.
@@ -227,8 +194,6 @@ 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."""
@@ -247,8 +212,7 @@ class Blink():
combined = merge_dicts(combined, self.sync[sync].cameras)
return combined
def download_videos(self, path, since=None,
camera='all', stop=10, debug=False):
def download_videos(self, path, since=None, camera='all', stop=10):
"""
Download all videos from server since specified time.
@@ -259,8 +223,6 @@ 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
@@ -278,23 +240,23 @@ class Blink():
response = api.request_videos(self, time=since_epochs, page=page)
_LOGGER.debug("Processing page %s", page)
try:
result = response['media']
result = response['videos']
if not result:
raise IndexError
except (KeyError, IndexError):
_LOGGER.info("No videos found on page %s. Exiting.", page)
break
self._parse_downloaded_items(result, camera, path, debug)
self._parse_downloaded_items(result, camera, path)
def _parse_downloaded_items(self, result, camera, path, debug):
def _parse_downloaded_items(self, result, camera, path):
"""Parse downloaded videos."""
for item in result:
try:
created_at = item['created_at']
camera_name = item['device_name']
camera_name = item['camera_name']
is_deleted = item['deleted']
address = item['media']
address = item['address']
except KeyError:
_LOGGER.info("Missing clip information, skipping...")
continue
@@ -313,17 +275,29 @@ class Blink():
filename = "{}_{}.mp4".format(camera_name, created_at)
filename = os.path.join(path, filename)
if not debug:
if os.path.isfile(filename):
_LOGGER.info("%s already exists, skipping...", filename)
continue
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)
else:
print("Camera: {}, Timestamp: {}, Address: {}".format(
camera_name, created_at, address))
_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)
+2 -3
View File
@@ -92,11 +92,10 @@ class BlinkCamera():
self.network_id,
self.camera_id)
def update(self, config, force_cache=False, **kwargs):
def update(self, config, force_cache=False):
"""Update camera info."""
# force = kwargs.pop('force', False)
self.name = config['name']
self.camera_id = str(config['id'])
self.camera_id = str(config['camera_id'])
self.network_id = str(config['network_id'])
self.serial = config['serial']
self.motion_enabled = config['enabled']
+3 -4
View File
@@ -3,8 +3,8 @@
import os
MAJOR_VERSION = 0
MINOR_VERSION = 14
PATCH_VERSION = '0.dev0'
MINOR_VERSION = 12
PATCH_VERSION = 0
__version__ = '{}.{}.{}'.format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
@@ -47,8 +47,7 @@ URLS
BLINK_URL = 'immedia-semi.com'
DEFAULT_URL = "{}.{}".format('prod', BLINK_URL)
BASE_URL = "https://{}".format(DEFAULT_URL)
LOGIN_URL = "{}/api/v2/login".format(BASE_URL)
OLD_LOGIN_URL = "{}/login".format(BASE_URL)
LOGIN_URL = "{}/login".format(BASE_URL)
LOGIN_BACKUP_URL = "https://{}.{}/login".format('rest.piri', BLINK_URL)
'''
+33
View File
@@ -0,0 +1,33 @@
"""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)
+2 -31
View File
@@ -2,7 +2,6 @@
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
@@ -107,6 +106,8 @@ class BlinkException(Exception):
class BlinkAuthenticationException(BlinkException):
"""Class to throw authentication exception."""
pass
class BlinkURLHandler():
"""Class that handles Blink URLS."""
@@ -120,33 +121,3 @@ class BlinkURLHandler():
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
+27 -43
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, camera_list):
def __init__(self, blink, network_name, network_id):
"""
Initialize Blink sync module.
@@ -30,12 +30,12 @@ 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 = {}
self.last_record = {}
self.camera_list = camera_list
@property
def attributes(self):
@@ -64,10 +64,7 @@ class BlinkSyncModule():
@property
def arm(self):
"""Return status of sync module: armed/disarmed."""
try:
return self.network_info['network']['armed']
except (KeyError, TypeError):
return None
return self.network_info['network']['armed']
@arm.setter
def arm(self, value):
@@ -79,8 +76,7 @@ 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']
@@ -98,35 +94,24 @@ 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()
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
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)
return True
def get_events(self, **kwargs):
def get_events(self):
"""Retrieve events from server."""
force = kwargs.pop('force', False)
response = api.request_sync_events(self.blink,
self.network_id,
force=force)
response = api.request_sync_events(self.blink, self.network_id)
try:
return response['event']
except (TypeError, KeyError):
@@ -135,13 +120,11 @@ class BlinkSyncModule():
exc_info=True)
return False
def get_camera_info(self, camera_id):
def get_camera_info(self):
"""Retrieve camera information."""
response = api.request_camera_info(self.blink,
self.network_id,
camera_id)
response = api.request_cameras(self.blink, self.network_id)
try:
return response['camera'][0]
return response['devicestatus']
except (TypeError, KeyError):
_LOGGER.error("Could not extract camera info: %s",
response,
@@ -150,34 +133,35 @@ 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_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)
for camera_config in camera_info:
name = camera_config['name']
self.cameras[name].update(camera_config, force_cache=force_cache)
def check_new_videos(self):
"""Check if new videos since last refresh."""
resp = api.request_videos(self.blink,
time=self.blink.last_refresh,
page=1)
page=0)
for camera in self.cameras.keys():
self.motion[camera] = False
try:
info = resp['media']
info = resp['videos']
except (KeyError, TypeError):
_LOGGER.warning("Could not check for motion. Response: %s", resp)
return False
for entry in info:
try:
name = entry['device_name']
clip = entry['media']
name = entry['camera_name']
clip = entry['address']
timestamp = entry['created_at']
self.motion[name] = True
self.last_record[name] = {'clip': clip, 'time': timestamp}
-4
View File
@@ -8,8 +8,6 @@ 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,
@@ -25,5 +23,3 @@ disable=
too-many-lines,
too-few-public-methods,
no-else-return,
no-self-use,
unexpected-keyword-arg,
+1 -1
View File
@@ -1,6 +1,6 @@
flake8==3.5.0
flake8-docstrings==1.3.0
pylint==2.3.0
pylint==2.1.1
pydocstyle==2.1.1
pytest==3.7.1
pytest-cov>=2.3.1
+2
View File
@@ -67,3 +67,5 @@ def mocked_session_send(*args, **kwargs):
class MockURLHandler(BlinkURLHandler):
"""Mocks URL Handler in blinkpy module."""
pass
+28 -44
View File
@@ -2,6 +2,7 @@
import unittest
from unittest import mock
import logging
from requests import Request
from blinkpy import blinkpy
from blinkpy.sync_module import BlinkSyncModule
@@ -55,39 +56,31 @@ class TestBlinkFunctions(unittest.TestCase):
"""Clean up after test."""
self.blink = None
@mock.patch('blinkpy.blinkpy.api.request_login')
@mock.patch('blinkpy.blinkpy.api.http_req')
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 = [
bad_req,
bad_req,
mresp.mocked_session_send(fake_req),
new_req
]
self.blink.login_request(['test1', 'test2', 'test3'])
self.blink.get_auth_token()
self.assertEqual(self.blink.region_id, 'piri')
self.assertEqual(self.blink.region, 'UNKNOWN')
# pylint: disable=protected-access
self.assertEqual(self.blink._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')
self.assertEqual(self.blink._token, 'foobar123')
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()
@@ -97,17 +90,14 @@ 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()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
blink = blinkpy.Blink(loglevel=logging.DEBUG)
blink.last_refresh = 0
mock_req.return_value = {}
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"INFO:blinkpy.blinkpy:No videos found on page 1. Exiting."
"INFO:blinkpy:Retrieving videos since {}".format(formatted_date),
"DEBUG:blinkpy:Processing page 1",
"INFO:blinkpy:No videos found on page 1. Exiting."
]
with self.assertLogs() as dl_log:
blink.download_videos('/tmp')
@@ -116,24 +106,21 @@ 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()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
blink = blinkpy.Blink(loglevel=logging.DEBUG)
generic_entry = {
'created_at': '1970',
'device_name': 'foo',
'camera_name': 'foo',
'deleted': True,
'media': '/bar.mp4'
'address': '/bar.mp4'
}
result = [generic_entry]
mock_req.return_value = {'media': result}
mock_req.return_value = {'videos': result}
blink.last_refresh = 0
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"DEBUG:blinkpy.blinkpy:foo: /bar.mp4 is marked as deleted."
"INFO:blinkpy:Retrieving videos since {}".format(formatted_date),
"DEBUG:blinkpy:Processing page 1",
"DEBUG:blinkpy:foo: /bar.mp4 is marked as deleted."
]
with self.assertLogs() as dl_log:
blink.download_videos('/tmp', stop=2)
@@ -142,24 +129,21 @@ 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()
# pylint: disable=protected-access
blinkpy._LOGGER.setLevel(logging.DEBUG)
blink = blinkpy.Blink(loglevel=logging.DEBUG)
generic_entry = {
'created_at': '1970',
'device_name': 'foo',
'camera_name': 'foo',
'deleted': True,
'media': '/bar.mp4'
'address': '/bar.mp4'
}
result = [generic_entry]
mock_req.return_value = {'media': result}
mock_req.return_value = {'videos': result}
blink.last_refresh = 0
formatted_date = get_time(blink.last_refresh)
expected_log = [
"INFO:blinkpy.blinkpy:Retrieving videos since {}".format(
formatted_date),
"DEBUG:blinkpy.blinkpy:Processing page 1",
"DEBUG:blinkpy.blinkpy:Skipping videos for foo."
"INFO:blinkpy:Retrieving videos since {}".format(formatted_date),
"DEBUG:blinkpy:Processing page 1",
"DEBUG:blinkpy:Skipping videos for foo."
]
with self.assertLogs() as dl_log:
blink.download_videos('/tmp', camera='bar', stop=2)
+1 -24
View File
@@ -31,10 +31,7 @@ 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()
@@ -163,23 +160,3 @@ 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, {})
+6 -6
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',
'id': 1234,
'camera_id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
@@ -115,7 +115,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
}
config = {
'name': 'new',
'id': 1234,
'camera_id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
@@ -144,7 +144,7 @@ class TestBlinkCameraSetup(unittest.TestCase):
self.camera.last_record = ['1']
config = {
'name': 'new',
'id': 1234,
'camera_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, force=True)
self.camera.update(config)
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',
'id': 1234,
'camera_id': 1234,
'network_id': 5678,
'serial': '12345678',
'enabled': False,
+21 -11
View File
@@ -24,10 +24,7 @@ class TestBlinkSyncModule(unittest.TestCase):
'TOKEN_AUTH': 'foobar123'
}
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': {
@@ -55,16 +52,15 @@ class TestBlinkSyncModule(unittest.TestCase):
def test_get_camera_info(self, mock_resp):
"""Test get camera info function."""
mock_resp.return_value = {'camera': ['foobar']}
self.assertEqual(self.blink.sync['test'].get_camera_info('1234'),
'foobar')
mock_resp.return_value = {'devicestatus': True}
self.assertEqual(self.blink.sync['test'].get_camera_info(), True)
def test_check_new_videos(self, mock_resp):
"""Test recent video response."""
mock_resp.return_value = {
'media': [{
'device_name': 'foo',
'media': '/foo/bar.mp4',
'videos': [{
'camera_name': 'foo',
'address': '/foo/bar.mp4',
'created_at': '1970-01-01T00:00:00+0:00'
}]
}
@@ -76,7 +72,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 = {'media': []}
mock_resp.return_value = {'videos': []}
self.assertTrue(sync_module.check_new_videos())
self.assertEqual(sync_module.motion, {'foo': False})
self.assertEqual(sync_module.last_record['foo'],
@@ -130,6 +126,20 @@ 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
-100
View File
@@ -1,100 +0,0 @@
"""Test various api functions."""
import unittest
from unittest import mock
import time
from blinkpy.helpers.util import Throttle
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)