+29
-9
@@ -38,29 +38,49 @@ This library was built with the intention of allowing easy communication with Bl
|
||||
|
||||
Usage
|
||||
=========
|
||||
The simplest way to use this package from a terminal is to call ``Blink.start()`` which will prompt for your Blink username and password and then log you in. Alternatively, you can instantiate the Blink class with a username and password, and call ``Blink.start()`` to login and setup without prompt, as shown below.
|
||||
The simplest way to use this package from a terminal is to call ``Blink.start()`` which will prompt for your Blink username and password and then log you in. Alternatively, you can instantiate the Blink class with a username and password, and call ``Blink.start()`` to login and setup without prompt, as shown below. In addition, http requests are throttled internally via use of the ``Blink.refresh_rate`` variable, which can be set at initialization and defaults to 30 seconds.
|
||||
|
||||
.. code:: python
|
||||
|
||||
import blinkpy
|
||||
blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD')
|
||||
from blinkpy import blinkpy
|
||||
blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD', refresh_rate=30)
|
||||
blink.start()
|
||||
|
||||
If you would like to log in without setting up the cameras or system, you can simply call the ``Blink.login()`` function which will prompt for a username and password and then authenticate with the server. This is useful if you want to avoid use of the ``start()`` function which simply acts as a wrapper for more targeted API methods.
|
||||
|
||||
The cameras are of a BlinkCamera class, of which the following parameters can be used (the code below creates a Blink object and iterates through each camera found)
|
||||
Cameras are instantiated as individual ``BlinkCamera`` classes within a ``BlinkSyncModule`` instance. Note: currently the API only supports one sync module, but multiple sync modules are planned to be supported in the future.
|
||||
|
||||
The below code will display cameras and their available attributes:
|
||||
|
||||
.. code:: python
|
||||
|
||||
import blinkpy
|
||||
|
||||
from blinkpy import blinkpy
|
||||
|
||||
blink = blinkpy.Blink(username='YOUR USER NAME', password='YOUR PASSWORD')
|
||||
blink.start()
|
||||
|
||||
for name, camera in blink.cameras.items():
|
||||
print(name) # Name of the camera
|
||||
|
||||
for name, camera in blink.sync.cameras.items():
|
||||
print(name) # Name of the camera
|
||||
print(camera.attributes) # Print available attributes of camera
|
||||
|
||||
The most recent images and videos can be accessed as a bytes-object via internal variables. These can be updated with calls to ``Blink.refresh()`` but will only make a request if motion has been detected or other changes have been found. This can be overridden with the ``force_cache`` flag, but this should be used for debugging only since it overrides the internal request throttling.
|
||||
|
||||
.. code:: python
|
||||
|
||||
camera = blink.sync.camera['SOME CAMERA NAME']
|
||||
blink.refresh(force_cache=True) # force a cache update USE WITH CAUTION
|
||||
camera.image_from_cache.raw # bytes-like image object (jpg)
|
||||
camera.video_from_cache.raw # bytes-like video object (mp4)
|
||||
|
||||
The ``blinkpy`` api also allows for saving images and videos to a file and snapping a new picture from the camera remotely:
|
||||
|
||||
.. code:: python
|
||||
|
||||
camera = blink.sync.camera['SOME CAMERA NAME']
|
||||
camera.snap_picture() # Take a new picture with the camera
|
||||
blink.refresh() # Get new information from server
|
||||
camera.image_to_file('/local/path/for/image.jpg')
|
||||
camera.video_to_file('/local/path/for/video.mp4')
|
||||
|
||||
.. |Build Status| image:: https://travis-ci.org/fronzbot/blinkpy.svg?branch=dev
|
||||
:target: https://travis-ci.org/fronzbot/blinkpy
|
||||
|
||||
+56
-496
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
blinkpy by Kevin Fronczak - A Blink camera Python library.
|
||||
@@ -12,301 +12,24 @@ owned by Immedia Inc., see www.blinkforhome.com for more information.
|
||||
I am in no way affiliated with Blink, nor Immedia Inc.
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
import getpass
|
||||
from shutil import copyfileobj
|
||||
import logging
|
||||
import time
|
||||
import requests
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
import blinkpy.helpers.errors as ERROR
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.helpers.util import (
|
||||
http_req, BlinkURLHandler, BlinkException, BlinkAuthenticationException)
|
||||
from blinkpy.helpers.constants import (
|
||||
BLINK_URL, LOGIN_URL, LOGIN_BACKUP_URL,
|
||||
DEFAULT_URL, ONLINE
|
||||
)
|
||||
DEFAULT_URL, BLINK_URL, LOGIN_URL, LOGIN_BACKUP_URL)
|
||||
|
||||
REFRESH_RATE = 30
|
||||
|
||||
_LOGGER = logging.getLogger('blinkpy')
|
||||
|
||||
MAX_CLIPS = 5
|
||||
REFRESH_RATE = 30
|
||||
|
||||
|
||||
def _attempt_reauthorization(blink):
|
||||
"""Attempt to refresh auth token and links."""
|
||||
_LOGGER.debug("Auth token expired, attempting reauthorization.")
|
||||
headers = blink.get_auth_token()
|
||||
blink.set_links()
|
||||
return headers
|
||||
|
||||
|
||||
def _request(blink, url='http://google.com', data=None, headers=None,
|
||||
reqtype='get', stream=False, json_resp=True, is_retry=False):
|
||||
"""Perform server requests and check if reauthorization neccessary."""
|
||||
if reqtype == 'post':
|
||||
response = requests.post(url, headers=headers,
|
||||
data=data)
|
||||
elif reqtype == 'get':
|
||||
response = requests.get(url, headers=headers,
|
||||
stream=stream)
|
||||
else:
|
||||
raise BlinkException(ERROR.REQUEST)
|
||||
|
||||
if json_resp and 'code' in response.json():
|
||||
if is_retry:
|
||||
raise BlinkAuthenticationException(
|
||||
(response.json()['code'], response.json()['message']))
|
||||
else:
|
||||
headers = _attempt_reauthorization(blink)
|
||||
return _request(blink, url=url, data=data, headers=headers,
|
||||
reqtype=reqtype, stream=stream,
|
||||
json_resp=json_resp, is_retry=True)
|
||||
# pylint: disable=no-else-return
|
||||
if json_resp:
|
||||
return response.json()
|
||||
else:
|
||||
return response
|
||||
|
||||
|
||||
# pylint: disable=super-init-not-called
|
||||
class BlinkException(Exception):
|
||||
"""Class to throw general blink exception."""
|
||||
|
||||
def __init__(self, errcode):
|
||||
"""Initialize BlinkException."""
|
||||
self.errid = errcode[0]
|
||||
self.message = errcode[1]
|
||||
|
||||
|
||||
class BlinkAuthenticationException(BlinkException):
|
||||
"""Class to throw authentication exception."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BlinkURLHandler():
|
||||
"""Class that handles Blink URLS."""
|
||||
|
||||
def __init__(self, region_id):
|
||||
"""Initialize the urls."""
|
||||
self.base_url = "https://rest.{}.{}".format(region_id, 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 BlinkCamera():
|
||||
"""Class to initialize individual camera."""
|
||||
|
||||
def __init__(self, config, blink):
|
||||
"""Initiailize BlinkCamera."""
|
||||
self.blink = blink
|
||||
self.urls = self.blink.urls
|
||||
self.id = str(config['device_id']) # pylint: disable=invalid-name
|
||||
self.name = config['name']
|
||||
self._status = config['active']
|
||||
self.thumbnail = "{}{}.jpg".format(self.urls.base_url,
|
||||
config['thumbnail'])
|
||||
self.clip = "{}{}".format(self.urls.base_url, config['video'])
|
||||
self.temperature = config['temp']
|
||||
self._battery_string = config['battery']
|
||||
self.notifications = config['notifications']
|
||||
self.motion = dict()
|
||||
self.header = None
|
||||
self.image_link = None
|
||||
self.arm_link = None
|
||||
self.region_id = config['region_id']
|
||||
self.battery_voltage = -180
|
||||
self.motion_detected = None
|
||||
self.wifi_strength = None
|
||||
self.camera_config = dict()
|
||||
self.motion_enabled = None
|
||||
self.last_record = list()
|
||||
self._cached_image = None
|
||||
self._cached_video = None
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
"""Return dictionary of all camera attributes."""
|
||||
attributes = {
|
||||
'name': self.name,
|
||||
'device_id': self.id,
|
||||
'status': self._status,
|
||||
'armed': self.armed,
|
||||
'temperature': self.temperature,
|
||||
'temperature_c': self.temperature_c,
|
||||
'battery': self.battery,
|
||||
'thumbnail': self.thumbnail,
|
||||
'video': self.clip,
|
||||
'motion_enabled': self.motion_enabled,
|
||||
'notifications': self.notifications,
|
||||
'motion_detected': self.motion_detected,
|
||||
'wifi_strength': self.wifi_strength,
|
||||
'network_id': self.blink.network_id,
|
||||
'last_record': self.last_record
|
||||
}
|
||||
return attributes
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Return camera status."""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def armed(self):
|
||||
"""Return camera arm status."""
|
||||
return True if self._status == 'armed' else False
|
||||
|
||||
@property
|
||||
def battery(self):
|
||||
"""Return battery level as percentage."""
|
||||
return round(self.battery_voltage / 180 * 100)
|
||||
|
||||
@property
|
||||
def battery_string(self):
|
||||
"""Return string indicating battery status."""
|
||||
status = "Unknown"
|
||||
if self._battery_string > 1 and self._battery_string <= 3:
|
||||
status = "OK"
|
||||
elif self._battery_string >= 0:
|
||||
status = "Low"
|
||||
return status
|
||||
|
||||
@property
|
||||
def temperature_c(self):
|
||||
"""Return temperature in celcius."""
|
||||
return round((self.temperature - 32) / 9.0 * 5.0, 1)
|
||||
|
||||
@property
|
||||
def image_from_cache(self):
|
||||
"""Return the most recently cached image."""
|
||||
if self._cached_image:
|
||||
return self._cached_image
|
||||
return None
|
||||
|
||||
@property
|
||||
def video_from_cache(self):
|
||||
"""Return the most recently cached video."""
|
||||
if self._cached_video:
|
||||
return self._cached_video
|
||||
return None
|
||||
|
||||
def snap_picture(self):
|
||||
"""Take a picture with camera to create a new thumbnail."""
|
||||
_request(self.blink, url=self.image_link,
|
||||
headers=self.header, reqtype='post')
|
||||
|
||||
def set_motion_detect(self, enable):
|
||||
"""Set motion detection."""
|
||||
url = self.arm_link
|
||||
if enable:
|
||||
_request(self.blink, url=url + 'enable',
|
||||
headers=self.header, reqtype='post')
|
||||
else:
|
||||
_request(self.blink, url=url + 'disable',
|
||||
headers=self.header, reqtype='post')
|
||||
|
||||
def update(self, values, force_cache=False, skip_cache=False):
|
||||
"""Update camera information."""
|
||||
self.name = values['name']
|
||||
self._status = values['active']
|
||||
self.clip = "{}{}".format(
|
||||
self.urls.base_url, values['video'])
|
||||
new_thumbnail = "{}{}.jpg".format(
|
||||
self.urls.base_url, values['thumbnail'])
|
||||
self._battery_string = values['battery']
|
||||
self.notifications = values['notifications']
|
||||
|
||||
update_cached_image = False
|
||||
if new_thumbnail != self.thumbnail or self._cached_image is None:
|
||||
update_cached_image = True
|
||||
self.thumbnail = new_thumbnail
|
||||
try:
|
||||
cfg = self.blink.camera_config_request(self.id)
|
||||
self.camera_config = cfg
|
||||
except requests.exceptions.RequestException as err:
|
||||
_LOGGER.warning("Could not get config for %s with id %s",
|
||||
self.name, self.id)
|
||||
_LOGGER.warning("Exception raised: %s", err)
|
||||
|
||||
try:
|
||||
self.battery_voltage = cfg['camera'][0]['battery_voltage']
|
||||
self.motion_enabled = cfg['camera'][0]['motion_alert']
|
||||
self.wifi_strength = cfg['camera'][0]['wifi_strength']
|
||||
self.temperature = cfg['camera'][0]['temperature']
|
||||
except KeyError:
|
||||
_LOGGER.warning("Problem extracting config for camera %s",
|
||||
self.name)
|
||||
|
||||
# Check if the most recent clip is included in the last_record list
|
||||
# and that the last_record list is populated
|
||||
try:
|
||||
records = sorted(self.blink.record_dates[self.name])
|
||||
new_clip = records.pop()
|
||||
if new_clip not in self.last_record and self.last_record:
|
||||
self.motion_detected = True
|
||||
self.last_record.insert(0, new_clip)
|
||||
if len(self.last_record) > MAX_CLIPS:
|
||||
self.last_record.pop()
|
||||
elif not self.last_record:
|
||||
self.last_record.insert(0, new_clip)
|
||||
self.motion_detected = False
|
||||
else:
|
||||
self.motion_detected = False
|
||||
except KeyError:
|
||||
_LOGGER.warning("Could not extract clip info from camera %s",
|
||||
self.name)
|
||||
|
||||
if not skip_cache:
|
||||
if update_cached_image or force_cache:
|
||||
self._cached_image = _request(
|
||||
self.blink, url=self.image_refresh(), headers=self.header,
|
||||
reqtype='get', stream=True, json_resp=False)
|
||||
if (self.clip is None) or self.motion_detected or force_cache:
|
||||
self._cached_video = _request(
|
||||
self.blink, url=self.clip, headers=self.header,
|
||||
reqtype='get', stream=True, json_resp=False)
|
||||
|
||||
def image_refresh(self):
|
||||
"""Refresh current thumbnail."""
|
||||
url = self.urls.home_url
|
||||
response = _request(self.blink, url=url, headers=self.header,
|
||||
reqtype='get')['devices']
|
||||
for element in response:
|
||||
try:
|
||||
if str(element['device_id']) == self.id:
|
||||
self.thumbnail = (
|
||||
"{}{}.jpg".format(
|
||||
self.urls.base_url, element['thumbnail'])
|
||||
)
|
||||
return self.thumbnail
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def image_to_file(self, path):
|
||||
"""Write image to file."""
|
||||
_LOGGER.debug("Writing image from %s to %s", self.name, path)
|
||||
response = self._cached_image
|
||||
if response.status_code == 200:
|
||||
with open(path, 'wb') as imgfile:
|
||||
copyfileobj(response.raw, imgfile)
|
||||
else:
|
||||
_LOGGER.error("Cannot write image to file, response %s",
|
||||
response.status_code)
|
||||
|
||||
def video_to_file(self, path):
|
||||
"""Write video to file."""
|
||||
_LOGGER.debug("Writing video from %s to %s", self.name, path)
|
||||
response = self._cached_video
|
||||
with open(path, 'wb') as vidfile:
|
||||
copyfileobj(response.raw, vidfile)
|
||||
|
||||
|
||||
class Blink():
|
||||
"""Class to initialize communication and sync module."""
|
||||
"""Class to initialize communication."""
|
||||
|
||||
def __init__(self, username=None, password=None,
|
||||
refresh_rate=REFRESH_RATE):
|
||||
@@ -315,190 +38,24 @@ class Blink():
|
||||
self._password = password
|
||||
self._token = None
|
||||
self._auth_header = None
|
||||
self.network_id = None
|
||||
self.account_id = None
|
||||
self.region = None
|
||||
self.region_id = None
|
||||
self._host = None
|
||||
self._events = []
|
||||
self.cameras = CaseInsensitiveDict({})
|
||||
self._idlookup = {}
|
||||
self._last_summary = None
|
||||
self._last_events = None
|
||||
self.network_id = None
|
||||
self.account_id = None
|
||||
self.urls = None
|
||||
self._video_count = 0
|
||||
self._all_videos = {}
|
||||
self._summary = None
|
||||
self.record_dates = dict()
|
||||
self.refresh_rate = refresh_rate
|
||||
self.sync = None
|
||||
self.region = None
|
||||
self.region_id = None
|
||||
self.last_refresh = None
|
||||
|
||||
@property
|
||||
def camera_thumbs(self):
|
||||
"""Return camera thumbnails."""
|
||||
self.refresh()
|
||||
data = {}
|
||||
for name, camera in self.cameras.items():
|
||||
data[name] = camera.thumbnail
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def id_table(self):
|
||||
"""Return id/camera pairs."""
|
||||
return self._idlookup
|
||||
|
||||
@property
|
||||
def video_count(self):
|
||||
"""Return number of videos on server."""
|
||||
url = "{}/count".format(self.urls.video_url)
|
||||
headers = self._auth_header
|
||||
self._video_count = _request(self, url=url, headers=headers,
|
||||
reqtype='get')['count']
|
||||
return self._video_count
|
||||
self.refresh_rate = refresh_rate
|
||||
|
||||
@property
|
||||
def events(self):
|
||||
"""Get all events on server."""
|
||||
return self._events
|
||||
|
||||
@property
|
||||
def online(self):
|
||||
"""Return boolean system online status."""
|
||||
return ONLINE[self._status_request()['syncmodule']['status']]
|
||||
|
||||
@property
|
||||
def videos(self):
|
||||
"""Return video list."""
|
||||
return self._all_videos
|
||||
|
||||
@property
|
||||
def summary(self):
|
||||
"""Get a full summary of device information."""
|
||||
return self._summary
|
||||
|
||||
@property
|
||||
def arm(self):
|
||||
"""Return status of sync module: armed/disarmed."""
|
||||
return self.summary['network']['armed']
|
||||
|
||||
@arm.setter
|
||||
def arm(self, value):
|
||||
"""Arm or disarm system."""
|
||||
if value:
|
||||
value_to_append = 'arm'
|
||||
else:
|
||||
value_to_append = 'disarm'
|
||||
url = "{}/{}/{}".format(self.urls.network_url,
|
||||
self.network_id,
|
||||
value_to_append)
|
||||
_request(self, url=url, headers=self._auth_header, reqtype='post')
|
||||
|
||||
def refresh(self, force_cache=False):
|
||||
"""Get all blink cameras and pulls their most recent status."""
|
||||
current_time = int(time.time())
|
||||
last_refresh = self.last_refresh
|
||||
if last_refresh is None:
|
||||
last_refresh = 0
|
||||
force_cache = True
|
||||
if current_time >= (last_refresh + self.refresh_rate):
|
||||
_LOGGER.debug("Attempting refresh of cameras.")
|
||||
self._summary = self._summary_request()
|
||||
self._events = self._events_request()
|
||||
response = self.summary['devices']
|
||||
self.get_videos()
|
||||
for name in self.cameras:
|
||||
camera = self.cameras[name]
|
||||
for element in response:
|
||||
try:
|
||||
if str(element['device_id']) == camera.id:
|
||||
element['video'] = self.videos[name][0]['clip']
|
||||
thumb = self.videos[name][0]['thumb']
|
||||
element['thumbnail'] = thumb
|
||||
camera.update(element, force_cache=force_cache)
|
||||
except KeyError:
|
||||
pass
|
||||
self.last_refresh = int(time.time())
|
||||
|
||||
def get_videos(self, start_page=0, end_page=1):
|
||||
"""Retrieve last recorded videos per camera."""
|
||||
videos = list()
|
||||
all_dates = dict()
|
||||
for page_num in range(start_page, end_page + 1):
|
||||
this_page = self._video_request(page_num)
|
||||
if not this_page:
|
||||
break
|
||||
videos.append(this_page)
|
||||
|
||||
for page in videos:
|
||||
_LOGGER.debug("Retrieved video page %s", page)
|
||||
for entry in page:
|
||||
camera_name = entry['camera_name']
|
||||
clip_addr = entry['address']
|
||||
thumb_addr = entry['thumbnail']
|
||||
clip_date = clip_addr.split('_')[-6:]
|
||||
clip_date = '_'.join(clip_date)
|
||||
clip_date = clip_date.split('.')[0]
|
||||
if camera_name not in all_dates:
|
||||
all_dates[camera_name] = list()
|
||||
all_dates[camera_name].append(clip_date)
|
||||
try:
|
||||
self._all_videos[camera_name].append(
|
||||
{
|
||||
'clip': clip_addr,
|
||||
'thumb': thumb_addr,
|
||||
}
|
||||
)
|
||||
except KeyError:
|
||||
self._all_videos[camera_name] = [
|
||||
{
|
||||
'clip': clip_addr,
|
||||
'thumb': thumb_addr,
|
||||
}
|
||||
]
|
||||
self.record_dates = all_dates
|
||||
|
||||
def get_cameras(self):
|
||||
"""Find and creates cameras."""
|
||||
self._summary = self._summary_request()
|
||||
response = self.summary['devices']
|
||||
for element in response:
|
||||
if ('device_type' in element and
|
||||
element['device_type'] == 'camera'):
|
||||
# Add region to config
|
||||
element['region_id'] = self.region_id
|
||||
try:
|
||||
name = element['name']
|
||||
element['video'] = self.videos[name][0]['clip']
|
||||
element['thumbnail'] = self.videos[name][0]['thumb']
|
||||
except KeyError:
|
||||
element['video'] = None
|
||||
element['thumbnail'] = None
|
||||
device = BlinkCamera(element, self)
|
||||
self.cameras[device.name] = device
|
||||
self._idlookup[device.id] = device.name
|
||||
self.refresh()
|
||||
|
||||
def set_links(self):
|
||||
"""Set access links and required headers for each camera in system."""
|
||||
for name in self.cameras:
|
||||
camera = self.cameras[name]
|
||||
network_id_url = "{}/{}".format(self.urls.network_url,
|
||||
self.network_id)
|
||||
image_url = "{}/camera/{}/thumbnail".format(network_id_url,
|
||||
camera.id)
|
||||
arm_url = "{}/camera/{}/".format(network_id_url,
|
||||
camera.id)
|
||||
camera.image_link = image_url
|
||||
camera.arm_link = arm_url
|
||||
camera.header = self._auth_header
|
||||
|
||||
def setup_system(self):
|
||||
"""Legacy method support."""
|
||||
_LOGGER.warning(
|
||||
("Blink.setup_system() will be deprecated in future release. "
|
||||
"Please use Blink.start() instead.")
|
||||
)
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Perform full system setup.
|
||||
@@ -512,11 +69,12 @@ class Blink():
|
||||
self.get_auth_token()
|
||||
|
||||
self.get_ids()
|
||||
self.get_videos()
|
||||
if self.video_count > 0:
|
||||
self.get_cameras()
|
||||
self.set_links()
|
||||
self._events = self._events_request()
|
||||
self.sync = BlinkSyncModule(self, self._auth_header)
|
||||
self.sync.get_videos()
|
||||
if self.sync.video_count > 0:
|
||||
self.sync.get_cameras()
|
||||
self.sync.set_links()
|
||||
self._events = self.events_request()
|
||||
|
||||
def login(self):
|
||||
"""Prompt user for username and password."""
|
||||
@@ -542,7 +100,7 @@ class Blink():
|
||||
"password": self._password,
|
||||
"client_specifier": "iPhone 9.2 | 2.2 | 222"
|
||||
})
|
||||
response = _request(self, url=LOGIN_URL, headers=headers,
|
||||
response = http_req(self, url=LOGIN_URL, headers=headers,
|
||||
data=data, json_resp=False, reqtype='post')
|
||||
if response.status_code == 200:
|
||||
response = response.json()
|
||||
@@ -553,7 +111,7 @@ class Blink():
|
||||
"when authenticating, "
|
||||
"trying new url"), response.status_code
|
||||
)
|
||||
response = _request(self, url=LOGIN_BACKUP_URL, headers=headers,
|
||||
response = http_req(self, url=LOGIN_BACKUP_URL, headers=headers,
|
||||
data=data, reqtype='post')
|
||||
self.region_id = 'piri'
|
||||
self.region = "UNKNOWN"
|
||||
@@ -574,45 +132,47 @@ class Blink():
|
||||
self.network_id = str(response['networks'][0]['id'])
|
||||
self.account_id = str(response['networks'][0]['account_id'])
|
||||
|
||||
def _video_request(self, page=0):
|
||||
"""Perform a request for videos."""
|
||||
url = "{}/page/{}".format(self.urls.video_url, page)
|
||||
headers = self._auth_header
|
||||
return _request(self, url=url, headers=headers, reqtype='get')
|
||||
|
||||
def _summary_request(self):
|
||||
"""Get blink summary."""
|
||||
url = self.urls.home_url
|
||||
headers = self._auth_header
|
||||
if headers is None:
|
||||
raise BlinkException(ERROR.AUTH_TOKEN)
|
||||
return _request(self, url=url, headers=headers, reqtype='get')
|
||||
|
||||
def _network_request(self):
|
||||
"""Get network and account information."""
|
||||
url = self.urls.networks_url
|
||||
headers = self._auth_header
|
||||
if headers is None:
|
||||
raise BlinkException(ERROR.AUTH_TOKEN)
|
||||
return _request(self, url=url, headers=headers, reqtype='get')
|
||||
return http_req(self, url=url, headers=headers, reqtype='get')
|
||||
|
||||
def _events_request(self):
|
||||
def events_request(self):
|
||||
"""Get events on server."""
|
||||
url = "{}/{}".format(self.urls.event_url, self.network_id)
|
||||
headers = self._auth_header
|
||||
return _request(self, url=url, headers=headers, reqtype='get')
|
||||
if self.check_if_ok_to_update() or self._last_events is None:
|
||||
self._last_events = http_req(
|
||||
self, url=url, headers=headers, reqtype='get')
|
||||
return self._last_events
|
||||
|
||||
def _status_request(self):
|
||||
"""Get syncmodule status."""
|
||||
url = "{}/{}/syncmodules".format(self.urls.network_url,
|
||||
self.network_id)
|
||||
def summary_request(self):
|
||||
"""Get blink summary."""
|
||||
url = self.urls.home_url
|
||||
headers = self._auth_header
|
||||
return _request(self, url=url, headers=headers, reqtype='get')
|
||||
if headers is None:
|
||||
raise BlinkException(ERROR.AUTH_TOKEN)
|
||||
if self.check_if_ok_to_update() or self._last_summary is None:
|
||||
self._last_summary = http_req(
|
||||
self, url=url, headers=headers, reqtype='get')
|
||||
return self._last_summary
|
||||
|
||||
def camera_config_request(self, camera_id):
|
||||
"""Retrieve more info about Blink config."""
|
||||
url = "{}/network/{}/camera/{}/config".format(self.urls.base_url,
|
||||
self.network_id,
|
||||
str(camera_id))
|
||||
headers = self._auth_header
|
||||
return _request(self, url=url, headers=headers, reqtype='get')
|
||||
def refresh(self, force_cache=False):
|
||||
"""Perform a system refresh."""
|
||||
if self.check_if_ok_to_update() or force_cache:
|
||||
_LOGGER.debug("Attempting refresh of cameras.")
|
||||
self.sync.refresh(force_cache=force_cache)
|
||||
|
||||
def check_if_ok_to_update(self):
|
||||
"""Check if it is ok to perform an http request."""
|
||||
current_time = int(time.time())
|
||||
last_refresh = self.last_refresh
|
||||
if last_refresh is None:
|
||||
last_refresh = 0
|
||||
if current_time >= (last_refresh + self.refresh_rate):
|
||||
self.last_refresh = current_time
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
"""Defines Blink cameras."""
|
||||
|
||||
from shutil import copyfileobj
|
||||
import logging
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MAX_CLIPS = 5
|
||||
|
||||
|
||||
class BlinkCamera():
|
||||
"""Class to initialize individual camera."""
|
||||
|
||||
def __init__(self, config, sync):
|
||||
"""Initiailize BlinkCamera."""
|
||||
self.sync = sync
|
||||
self.urls = self.sync.urls
|
||||
self.id = str(config['device_id']) # pylint: disable=invalid-name
|
||||
self.name = config['name']
|
||||
self._status = config['active']
|
||||
self.thumbnail = "{}{}.jpg".format(self.urls.base_url,
|
||||
config['thumbnail'])
|
||||
self.clip = "{}{}".format(self.urls.base_url, config['video'])
|
||||
self.temperature = config['temp']
|
||||
self._battery_string = config['battery']
|
||||
self.notifications = config['notifications']
|
||||
self.motion = dict()
|
||||
self.header = None
|
||||
self.image_link = None
|
||||
self.arm_link = None
|
||||
self.region_id = config['region_id']
|
||||
self.battery_voltage = -180
|
||||
self.motion_detected = None
|
||||
self.wifi_strength = None
|
||||
self.camera_config = dict()
|
||||
self.motion_enabled = None
|
||||
self.last_record = list()
|
||||
self._cached_image = None
|
||||
self._cached_video = None
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
"""Return dictionary of all camera attributes."""
|
||||
attributes = {
|
||||
'name': self.name,
|
||||
'device_id': self.id,
|
||||
'status': self._status,
|
||||
'armed': self.armed,
|
||||
'temperature': self.temperature,
|
||||
'temperature_c': self.temperature_c,
|
||||
'battery': self.battery,
|
||||
'thumbnail': self.thumbnail,
|
||||
'video': self.clip,
|
||||
'motion_enabled': self.motion_enabled,
|
||||
'notifications': self.notifications,
|
||||
'motion_detected': self.motion_detected,
|
||||
'wifi_strength': self.wifi_strength,
|
||||
'network_id': self.sync.network_id,
|
||||
'last_record': self.last_record
|
||||
}
|
||||
return attributes
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Return camera status."""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def armed(self):
|
||||
"""Return camera arm status."""
|
||||
return True if self._status == 'armed' else False
|
||||
|
||||
@property
|
||||
def battery(self):
|
||||
"""Return battery level as percentage."""
|
||||
return round(self.battery_voltage / 180 * 100)
|
||||
|
||||
@property
|
||||
def battery_string(self):
|
||||
"""Return string indicating battery status."""
|
||||
status = "Unknown"
|
||||
if self._battery_string > 1 and self._battery_string <= 3:
|
||||
status = "OK"
|
||||
elif self._battery_string >= 0:
|
||||
status = "Low"
|
||||
return status
|
||||
|
||||
@property
|
||||
def temperature_c(self):
|
||||
"""Return temperature in celcius."""
|
||||
return round((self.temperature - 32) / 9.0 * 5.0, 1)
|
||||
|
||||
@property
|
||||
def image_from_cache(self):
|
||||
"""Return the most recently cached image."""
|
||||
if self._cached_image:
|
||||
return self._cached_image
|
||||
return None
|
||||
|
||||
@property
|
||||
def video_from_cache(self):
|
||||
"""Return the most recently cached video."""
|
||||
if self._cached_video:
|
||||
return self._cached_video
|
||||
return None
|
||||
|
||||
def snap_picture(self):
|
||||
"""Take a picture with camera to create a new thumbnail."""
|
||||
self.sync.http_post(self.image_link)
|
||||
|
||||
def set_motion_detect(self, enable):
|
||||
"""Set motion detection."""
|
||||
url = self.arm_link
|
||||
if enable:
|
||||
self.sync.http_post("{}{}".format(url, 'enable'))
|
||||
else:
|
||||
self.sync.http_post("{}{}".format(url, 'disable'))
|
||||
|
||||
def update(self, values, force_cache=False, skip_cache=False):
|
||||
"""Update camera information."""
|
||||
self.name = values['name']
|
||||
self._status = values['active']
|
||||
self.clip = "{}{}".format(
|
||||
self.urls.base_url, values['video'])
|
||||
new_thumbnail = "{}{}.jpg".format(
|
||||
self.urls.base_url, values['thumbnail'])
|
||||
self._battery_string = values['battery']
|
||||
self.notifications = values['notifications']
|
||||
|
||||
update_cached_image = False
|
||||
if new_thumbnail != self.thumbnail or self._cached_image is None:
|
||||
update_cached_image = True
|
||||
self.thumbnail = new_thumbnail
|
||||
try:
|
||||
cfg = self.sync.camera_config_request(self.id)
|
||||
self.camera_config = cfg
|
||||
except RequestException as err:
|
||||
_LOGGER.warning("Could not get config for %s with id %s",
|
||||
self.name, self.id)
|
||||
_LOGGER.warning("Exception raised: %s", err)
|
||||
|
||||
try:
|
||||
self.battery_voltage = cfg['camera'][0]['battery_voltage']
|
||||
self.motion_enabled = cfg['camera'][0]['motion_alert']
|
||||
self.wifi_strength = cfg['camera'][0]['wifi_strength']
|
||||
self.temperature = cfg['camera'][0]['temperature']
|
||||
except KeyError:
|
||||
_LOGGER.warning("Problem extracting config for camera %s",
|
||||
self.name)
|
||||
|
||||
# Check if the most recent clip is included in the last_record list
|
||||
# and that the last_record list is populated
|
||||
try:
|
||||
records = sorted(self.sync.record_dates[self.name])
|
||||
new_clip = records.pop()
|
||||
if new_clip not in self.last_record and self.last_record:
|
||||
self.motion_detected = True
|
||||
self.last_record.insert(0, new_clip)
|
||||
if len(self.last_record) > MAX_CLIPS:
|
||||
self.last_record.pop()
|
||||
elif not self.last_record:
|
||||
self.last_record.insert(0, new_clip)
|
||||
self.motion_detected = False
|
||||
else:
|
||||
self.motion_detected = False
|
||||
except KeyError:
|
||||
_LOGGER.warning("Could not extract clip info from camera %s",
|
||||
self.name)
|
||||
|
||||
if not skip_cache:
|
||||
if update_cached_image or force_cache:
|
||||
self._cached_image = self.sync.http_get(
|
||||
self.image_refresh(), stream=True, json=False)
|
||||
if (self.clip is None) or self.motion_detected or force_cache:
|
||||
self._cached_video = self.sync.http_get(
|
||||
self.clip, stream=True, json=False)
|
||||
|
||||
def image_refresh(self):
|
||||
"""Refresh current thumbnail."""
|
||||
url = self.urls.home_url
|
||||
response = self.sync.http_get(url)['devices']
|
||||
for element in response:
|
||||
try:
|
||||
if str(element['device_id']) == self.id:
|
||||
self.thumbnail = (
|
||||
"{}{}.jpg".format(
|
||||
self.urls.base_url, element['thumbnail'])
|
||||
)
|
||||
return self.thumbnail
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def image_to_file(self, path):
|
||||
"""Write image to file."""
|
||||
_LOGGER.debug("Writing image from %s to %s", self.name, path)
|
||||
response = self._cached_image
|
||||
if response.status_code == 200:
|
||||
with open(path, 'wb') as imgfile:
|
||||
copyfileobj(response.raw, imgfile)
|
||||
else:
|
||||
_LOGGER.error("Cannot write image to file, response %s",
|
||||
response.status_code)
|
||||
|
||||
def video_to_file(self, path):
|
||||
"""Write video to file."""
|
||||
_LOGGER.debug("Writing video from %s to %s", self.name, path)
|
||||
response = self._cached_video
|
||||
with open(path, 'wb') as vidfile:
|
||||
copyfileobj(response.raw, vidfile)
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Useful functions for blinkpy."""
|
||||
|
||||
import logging
|
||||
import requests
|
||||
import blinkpy.helpers.errors as ERROR
|
||||
from blinkpy.helpers.constants import BLINK_URL
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def attempt_reauthorization(blink):
|
||||
"""Attempt to refresh auth token and links."""
|
||||
_LOGGER.debug("Auth token expired, attempting reauthorization.")
|
||||
headers = blink.get_auth_token()
|
||||
blink.sync.set_links()
|
||||
return headers
|
||||
|
||||
|
||||
def http_req(blink, url='http://google.com', data=None, headers=None,
|
||||
reqtype='get', stream=False, json_resp=True, is_retry=False):
|
||||
"""Perform server requests and check if reauthorization neccessary."""
|
||||
if reqtype == 'post':
|
||||
response = requests.post(url, headers=headers,
|
||||
data=data)
|
||||
elif reqtype == 'get':
|
||||
response = requests.get(url, headers=headers,
|
||||
stream=stream)
|
||||
else:
|
||||
raise BlinkException(ERROR.REQUEST)
|
||||
|
||||
if json_resp and 'code' in response.json():
|
||||
if is_retry:
|
||||
raise BlinkAuthenticationException(
|
||||
(response.json()['code'], response.json()['message']))
|
||||
else:
|
||||
headers = attempt_reauthorization(blink)
|
||||
return http_req(blink, url=url, data=data, headers=headers,
|
||||
reqtype=reqtype, stream=stream,
|
||||
json_resp=json_resp, is_retry=True)
|
||||
# pylint: disable=no-else-return
|
||||
if json_resp:
|
||||
return response.json()
|
||||
else:
|
||||
return response
|
||||
|
||||
|
||||
# pylint: disable=super-init-not-called
|
||||
class BlinkException(Exception):
|
||||
"""Class to throw general blink exception."""
|
||||
|
||||
def __init__(self, errcode):
|
||||
"""Initialize BlinkException."""
|
||||
self.errid = errcode[0]
|
||||
self.message = errcode[1]
|
||||
|
||||
|
||||
class BlinkAuthenticationException(BlinkException):
|
||||
"""Class to throw authentication exception."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BlinkURLHandler():
|
||||
"""Class that handles Blink URLS."""
|
||||
|
||||
def __init__(self, region_id):
|
||||
"""Initialize the urls."""
|
||||
self.base_url = "https://rest.{}.{}".format(region_id, 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)
|
||||
@@ -0,0 +1,228 @@
|
||||
"""Defines a sync module for Blink."""
|
||||
|
||||
import logging
|
||||
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
from blinkpy.camera import BlinkCamera
|
||||
from blinkpy.helpers.util import http_req
|
||||
from blinkpy.helpers.constants import ONLINE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BlinkSyncModule():
|
||||
"""Class to initialize sync module."""
|
||||
|
||||
def __init__(self, blink, header, urls=None):
|
||||
"""Initialize Blink sync module."""
|
||||
self.blink = blink
|
||||
self._auth_header = header
|
||||
self.sync_id = None
|
||||
self.region = None
|
||||
self.region_id = None
|
||||
self._host = None
|
||||
self._events = []
|
||||
self.cameras = CaseInsensitiveDict({})
|
||||
self._idlookup = {}
|
||||
self._video_count = 0
|
||||
self._all_videos = {}
|
||||
self._summary = None
|
||||
self.record_dates = dict()
|
||||
self.first_init = True
|
||||
|
||||
@property
|
||||
def urls(self):
|
||||
"""Return device urls."""
|
||||
return self.blink.urls
|
||||
|
||||
@property
|
||||
def network_id(self):
|
||||
"""Return the network id."""
|
||||
return self.blink.network_id
|
||||
|
||||
@property
|
||||
def camera_thumbs(self):
|
||||
"""Return camera thumbnails."""
|
||||
self.blink.refresh()
|
||||
data = {}
|
||||
for name, camera in self.cameras.items():
|
||||
data[name] = camera.thumbnail
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def id_table(self):
|
||||
"""Return id/camera pairs."""
|
||||
return self._idlookup
|
||||
|
||||
@property
|
||||
def video_count(self):
|
||||
"""Return number of videos on server."""
|
||||
url = "{}/count".format(self.urls.video_url)
|
||||
self._video_count = self.http_get(url)['count']
|
||||
return self._video_count
|
||||
|
||||
@property
|
||||
def events(self):
|
||||
"""Get all events on server."""
|
||||
return self._events
|
||||
|
||||
@property
|
||||
def online(self):
|
||||
"""Return boolean system online status."""
|
||||
return ONLINE[self._status_request()['syncmodule']['status']]
|
||||
|
||||
@property
|
||||
def videos(self):
|
||||
"""Return video list."""
|
||||
return self._all_videos
|
||||
|
||||
@property
|
||||
def summary(self):
|
||||
"""Get a full summary of device information."""
|
||||
return self._summary
|
||||
|
||||
@property
|
||||
def arm(self):
|
||||
"""Return status of sync module: armed/disarmed."""
|
||||
return self.summary['network']['armed']
|
||||
|
||||
@arm.setter
|
||||
def arm(self, value):
|
||||
"""Arm or disarm system."""
|
||||
if value:
|
||||
value_to_append = 'arm'
|
||||
else:
|
||||
value_to_append = 'disarm'
|
||||
url = "{}/{}/{}".format(self.urls.network_url,
|
||||
self.network_id,
|
||||
value_to_append)
|
||||
self.http_post(url)
|
||||
|
||||
def refresh(self, force_cache=False):
|
||||
"""Get all blink cameras and pulls their most recent status."""
|
||||
self._summary = self._summary_request()
|
||||
self._events = self._events_request()
|
||||
response = self.summary['devices']
|
||||
self.get_videos()
|
||||
for name in self.cameras:
|
||||
camera = self.cameras[name]
|
||||
for element in response:
|
||||
try:
|
||||
if str(element['device_id']) == camera.id:
|
||||
element['video'] = self.videos[name][0]['clip']
|
||||
thumb = self.videos[name][0]['thumb']
|
||||
element['thumbnail'] = thumb
|
||||
camera.update(element, force_cache=force_cache)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def get_videos(self, start_page=0, end_page=1):
|
||||
"""Retrieve last recorded videos per camera."""
|
||||
videos = list()
|
||||
all_dates = dict()
|
||||
for page_num in range(start_page, end_page + 1):
|
||||
this_page = self._video_request(page_num)
|
||||
if not this_page:
|
||||
break
|
||||
videos.append(this_page)
|
||||
|
||||
for page in videos:
|
||||
_LOGGER.debug("Retrieved video page %s", page)
|
||||
for entry in page:
|
||||
camera_name = entry['camera_name']
|
||||
clip_addr = entry['address']
|
||||
thumb_addr = entry['thumbnail']
|
||||
clip_date = clip_addr.split('_')[-6:]
|
||||
clip_date = '_'.join(clip_date)
|
||||
clip_date = clip_date.split('.')[0]
|
||||
if camera_name not in all_dates:
|
||||
all_dates[camera_name] = list()
|
||||
all_dates[camera_name].append(clip_date)
|
||||
try:
|
||||
self._all_videos[camera_name].append(
|
||||
{
|
||||
'clip': clip_addr,
|
||||
'thumb': thumb_addr,
|
||||
}
|
||||
)
|
||||
except KeyError:
|
||||
self._all_videos[camera_name] = [
|
||||
{
|
||||
'clip': clip_addr,
|
||||
'thumb': thumb_addr,
|
||||
}
|
||||
]
|
||||
self.record_dates = all_dates
|
||||
|
||||
def get_cameras(self):
|
||||
"""Find and creates cameras."""
|
||||
self._summary = self._summary_request()
|
||||
response = self.summary['devices']
|
||||
for element in response:
|
||||
if ('device_type' in element and
|
||||
element['device_type'] == 'camera'):
|
||||
# Add region to config
|
||||
element['region_id'] = self.region_id
|
||||
try:
|
||||
name = element['name']
|
||||
element['video'] = self.videos[name][0]['clip']
|
||||
element['thumbnail'] = self.videos[name][0]['thumb']
|
||||
except KeyError:
|
||||
element['video'] = None
|
||||
element['thumbnail'] = None
|
||||
device = BlinkCamera(element, self)
|
||||
self.cameras[device.name] = device
|
||||
self._idlookup[device.id] = device.name
|
||||
self.blink.refresh(force_cache=self.first_init)
|
||||
self.first_init = False
|
||||
|
||||
def set_links(self):
|
||||
"""Set access links and required headers for each camera in system."""
|
||||
for name in self.cameras:
|
||||
camera = self.cameras[name]
|
||||
network_id_url = "{}/{}".format(self.urls.network_url,
|
||||
self.network_id)
|
||||
image_url = "{}/camera/{}/thumbnail".format(network_id_url,
|
||||
camera.id)
|
||||
arm_url = "{}/camera/{}/".format(network_id_url,
|
||||
camera.id)
|
||||
camera.image_link = image_url
|
||||
camera.arm_link = arm_url
|
||||
camera.header = self._auth_header
|
||||
|
||||
def _summary_request(self):
|
||||
"""Request a summary from blink."""
|
||||
return self.blink.summary_request()
|
||||
|
||||
def _events_request(self):
|
||||
"""Request a list of events from blink."""
|
||||
return self.blink.events_request()
|
||||
|
||||
def _video_request(self, page=0):
|
||||
"""Perform a request for videos."""
|
||||
url = "{}/page/{}".format(self.urls.video_url, page)
|
||||
return self.http_get(url)
|
||||
|
||||
def _status_request(self):
|
||||
"""Get syncmodule status."""
|
||||
url = "{}/{}/syncmodules".format(self.urls.network_url,
|
||||
self.network_id)
|
||||
return self.http_get(url)
|
||||
|
||||
def camera_config_request(self, camera_id):
|
||||
"""Retrieve more info about Blink config."""
|
||||
url = "{}/network/{}/camera/{}/config".format(self.urls.base_url,
|
||||
self.network_id,
|
||||
str(camera_id))
|
||||
return self.http_get(url)
|
||||
|
||||
def http_get(self, url, stream=False, json=True):
|
||||
"""Perform a get request."""
|
||||
return http_req(self.blink, url=url, headers=self._auth_header,
|
||||
reqtype='get', stream=stream, json_resp=json)
|
||||
|
||||
def http_post(self, url):
|
||||
"""Perform a post request."""
|
||||
return http_req(self.blink, url=url, headers=self._auth_header,
|
||||
reqtype='post')
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Simple mock responses definitions."""
|
||||
|
||||
from blinkpy.blinkpy import BlinkURLHandler
|
||||
from blinkpy.helpers.util import BlinkURLHandler
|
||||
import blinkpy.helpers.constants as const
|
||||
|
||||
LOGIN_RESPONSE = {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
"""Tests camera and system functions."""
|
||||
import time
|
||||
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
# import pytest
|
||||
|
||||
from blinkpy import blinkpy
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.camera import BlinkCamera
|
||||
from blinkpy.helpers.constants import BLINK_URL
|
||||
import tests.mock_responses as mresp
|
||||
|
||||
@@ -10,6 +16,28 @@ USERNAME = 'foobar'
|
||||
PASSWORD = 'deadbeef'
|
||||
|
||||
|
||||
class MockSyncModule(BlinkSyncModule):
|
||||
"""Mock http requests from sync module."""
|
||||
|
||||
def __init__(self, blink, header):
|
||||
"""Create mock sync module instance."""
|
||||
super().__init__(blink, header)
|
||||
self.blink = blink
|
||||
self.header = header
|
||||
self.return_value = None
|
||||
self.return_value2 = None
|
||||
|
||||
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 TestBlinkFunctions(unittest.TestCase):
|
||||
"""Test Blink and BlinkCamera functions in blinkpy."""
|
||||
|
||||
@@ -36,7 +64,10 @@ class TestBlinkFunctions(unittest.TestCase):
|
||||
'region_id': 'test',
|
||||
'device_type': 'camera'
|
||||
}
|
||||
self.camera = blinkpy.BlinkCamera(self.config, self.blink)
|
||||
self.blink.sync = MockSyncModule(
|
||||
self.blink, self.blink._auth_header)
|
||||
|
||||
self.camera = BlinkCamera(self.config, self.blink.sync)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after test."""
|
||||
@@ -44,66 +75,35 @@ class TestBlinkFunctions(unittest.TestCase):
|
||||
self.config = {}
|
||||
self.camera = None
|
||||
|
||||
@mock.patch('blinkpy.blinkpy._request')
|
||||
def test_get_videos(self, req):
|
||||
"""Test video access."""
|
||||
req.return_value = [
|
||||
{
|
||||
'camera_name': 'foobar',
|
||||
'address': '/new/test.mp4',
|
||||
'thumbnail': '/test/thumb'
|
||||
}
|
||||
]
|
||||
self.blink.get_videos()
|
||||
self.assertEqual(self.blink.videos['foobar'][0]['clip'],
|
||||
'/new/test.mp4')
|
||||
self.assertEqual(self.blink.videos['foobar'][0]['thumb'],
|
||||
'/test/thumb')
|
||||
|
||||
@mock.patch('blinkpy.blinkpy._request')
|
||||
@mock.patch('blinkpy.blinkpy.Blink._video_request')
|
||||
def test_get_cameras(self, vid_req, req):
|
||||
"""Test camera extraction."""
|
||||
req.return_value = {'devices': [self.config]}
|
||||
vid_req.return_value = [
|
||||
{
|
||||
'camera_name': 'foobar',
|
||||
'address': '/new.mp4',
|
||||
'thumbnail': '/new'
|
||||
}
|
||||
]
|
||||
self.blink.get_cameras()
|
||||
self.assertTrue('foobar' in self.blink.cameras)
|
||||
|
||||
@mock.patch('blinkpy.blinkpy._request')
|
||||
def test_image_refresh(self, req):
|
||||
def test_image_refresh(self):
|
||||
"""Test image refresh function."""
|
||||
req.return_value = {'devices': [self.config]}
|
||||
self.blink.sync.return_value = {'devices': [self.config]}
|
||||
image = self.camera.image_refresh()
|
||||
self.assertEqual(image,
|
||||
'https://rest.test.{}/test.jpg'.format(BLINK_URL))
|
||||
|
||||
@mock.patch('blinkpy.blinkpy._request')
|
||||
def test_video_count(self, req):
|
||||
"""Test video count function."""
|
||||
req.return_value = {'count': 1}
|
||||
self.assertEqual(self.blink.video_count, 1)
|
||||
|
||||
@mock.patch('blinkpy.blinkpy._request')
|
||||
@mock.patch('blinkpy.blinkpy.Blink._video_request')
|
||||
@mock.patch('blinkpy.sync_module.BlinkSyncModule.camera_config_request')
|
||||
@mock.patch('blinkpy.sync_module.BlinkSyncModule._video_request')
|
||||
def test_refresh(self, vid_req, req):
|
||||
"""Test blinkpy refresh function."""
|
||||
self.blink.cameras = {'foobar': self.camera}
|
||||
req.return_value = {'devices': [self.config]}
|
||||
req.return_value = {'foo': 'bar'}
|
||||
self.blink.sync.cameras = {'foobar': self.camera}
|
||||
self.blink.sync.return_value = {'devices': [{'foo': 'bar'}]}
|
||||
# pylint: disable=protected-access
|
||||
self.blink._last_summary = {'devices': [self.config]}
|
||||
# pylint: disable=protected-access
|
||||
self.blink._last_events = {'foo': 'bar'}
|
||||
vid_req.return_value = [
|
||||
{
|
||||
'camera_name': 'foobar',
|
||||
'address': '/new.mp4',
|
||||
'thumbnail': '/new'
|
||||
'thumbnail': '/new',
|
||||
}
|
||||
]
|
||||
self.blink.refresh()
|
||||
test_camera = self.blink.cameras['foobar']
|
||||
self.blink.last_refresh = int(time.time())
|
||||
self.blink.refresh_rate = 100000
|
||||
self.blink.sync.refresh()
|
||||
test_camera = self.blink.sync.cameras['foobar']
|
||||
self.assertEqual(test_camera.clip,
|
||||
'https://rest.test.{}/new.mp4'.format(BLINK_URL))
|
||||
self.assertEqual(test_camera.thumbnail,
|
||||
@@ -111,16 +111,16 @@ class TestBlinkFunctions(unittest.TestCase):
|
||||
|
||||
def test_set_links(self):
|
||||
"""Test the link set method."""
|
||||
self.blink.cameras = {'foobar': self.camera}
|
||||
self.blink.sync.cameras = {'foobar': self.camera}
|
||||
self.blink.network_id = 9999
|
||||
self.blink.set_links()
|
||||
self.blink.sync.set_links()
|
||||
net_url = "{}/{}".format(self.blink.urls.network_url, 9999)
|
||||
self.assertEqual(self.camera.image_link,
|
||||
"{}/camera/1111/thumbnail".format(net_url))
|
||||
self.assertEqual(self.camera.arm_link,
|
||||
"{}/camera/1111/".format(net_url))
|
||||
|
||||
@mock.patch('blinkpy.blinkpy._request')
|
||||
@mock.patch('blinkpy.blinkpy.http_req')
|
||||
def test_backup_url(self, req):
|
||||
"""Test backup login method."""
|
||||
req.side_effect = [
|
||||
|
||||
+23
-20
@@ -9,6 +9,10 @@ any communication related errors at startup.
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from blinkpy import blinkpy
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.helpers.util import (
|
||||
http_req, BlinkAuthenticationException, BlinkException,
|
||||
BlinkURLHandler)
|
||||
import tests.mock_responses as mresp
|
||||
|
||||
USERNAME = 'foobar'
|
||||
@@ -23,6 +27,7 @@ class TestBlinkSetup(unittest.TestCase):
|
||||
self.blink_no_cred = blinkpy.Blink()
|
||||
self.blink = blinkpy.Blink(username=USERNAME,
|
||||
password=PASSWORD)
|
||||
self.blink.sync = BlinkSyncModule(self.blink, dict(), self.blink.urls)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after test."""
|
||||
@@ -38,25 +43,24 @@ class TestBlinkSetup(unittest.TestCase):
|
||||
|
||||
def test_no_credentials(self):
|
||||
"""Check that we throw an exception when no username/password."""
|
||||
with self.assertRaises(blinkpy.BlinkAuthenticationException):
|
||||
with self.assertRaises(BlinkAuthenticationException):
|
||||
self.blink_no_cred.get_auth_token()
|
||||
# pylint: disable=protected-access
|
||||
self.blink_no_cred._username = USERNAME
|
||||
with self.assertRaises(blinkpy.BlinkAuthenticationException):
|
||||
with self.assertRaises(BlinkAuthenticationException):
|
||||
self.blink_no_cred.get_auth_token()
|
||||
|
||||
def test_no_auth_header(self):
|
||||
"""Check that we throw an exception when no auth header given."""
|
||||
# pylint: disable=unused-variable
|
||||
(region_id, region), = mresp.LOGIN_RESPONSE['region'].items()
|
||||
self.blink.urls = blinkpy.BlinkURLHandler(region_id)
|
||||
with self.assertRaises(blinkpy.BlinkException):
|
||||
self.blink.urls = BlinkURLHandler(region_id)
|
||||
with self.assertRaises(BlinkException):
|
||||
self.blink.get_ids()
|
||||
with self.assertRaises(blinkpy.BlinkException):
|
||||
# pylint: disable=protected-access
|
||||
self.blink._summary_request()
|
||||
with self.assertRaises(BlinkException):
|
||||
self.blink.summary_request()
|
||||
|
||||
@mock.patch('blinkpy.blinkpy.requests.post',
|
||||
@mock.patch('blinkpy.helpers.util.requests.post',
|
||||
side_effect=mresp.mocked_requests_post)
|
||||
@mock.patch('blinkpy.blinkpy.getpass.getpass')
|
||||
def test_manual_login(self, getpwd, mock_post):
|
||||
@@ -69,23 +73,23 @@ class TestBlinkSetup(unittest.TestCase):
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(self.blink_no_cred._password, PASSWORD)
|
||||
|
||||
@mock.patch('blinkpy.blinkpy.requests.post',
|
||||
@mock.patch('blinkpy.helpers.util.requests.post',
|
||||
side_effect=mresp.mocked_requests_post)
|
||||
@mock.patch('blinkpy.blinkpy.requests.get',
|
||||
@mock.patch('blinkpy.helpers.util.requests.get',
|
||||
side_effect=mresp.mocked_requests_get)
|
||||
def test_bad_request(self, mock_get, mock_post):
|
||||
"""Check that we raise an Exception with a bad request."""
|
||||
with self.assertRaises(blinkpy.BlinkException):
|
||||
with self.assertRaises(BlinkException):
|
||||
# pylint: disable=protected-access
|
||||
blinkpy._request(None, reqtype='bad')
|
||||
http_req(None, reqtype='bad')
|
||||
|
||||
with self.assertRaises(blinkpy.BlinkAuthenticationException):
|
||||
with self.assertRaises(BlinkAuthenticationException):
|
||||
# pylint: disable=protected-access
|
||||
blinkpy._request(None, reqtype='post', is_retry=True)
|
||||
http_req(None, reqtype='post', is_retry=True)
|
||||
|
||||
@mock.patch('blinkpy.blinkpy.requests.post',
|
||||
@mock.patch('blinkpy.helpers.util.requests.post',
|
||||
side_effect=mresp.mocked_requests_post)
|
||||
@mock.patch('blinkpy.blinkpy.requests.get',
|
||||
@mock.patch('blinkpy.helpers.util.requests.get',
|
||||
side_effect=mresp.mocked_requests_get)
|
||||
def test_authentication(self, mock_get, mock_post):
|
||||
"""Check that we can authenticate Blink up properly."""
|
||||
@@ -93,9 +97,9 @@ class TestBlinkSetup(unittest.TestCase):
|
||||
expected = mresp.LOGIN_RESPONSE['authtoken']['authtoken']
|
||||
self.assertEqual(authtoken, expected)
|
||||
|
||||
@mock.patch('blinkpy.blinkpy.requests.post',
|
||||
@mock.patch('blinkpy.helpers.util.requests.post',
|
||||
side_effect=mresp.mocked_requests_post)
|
||||
@mock.patch('blinkpy.blinkpy.requests.get',
|
||||
@mock.patch('blinkpy.helpers.util.requests.get',
|
||||
side_effect=mresp.mocked_requests_get)
|
||||
def test_reauthorization_attempt(self, mock_get, mock_post):
|
||||
"""Check that we can reauthorize after first unsuccessful attempt."""
|
||||
@@ -106,7 +110,6 @@ class TestBlinkSetup(unittest.TestCase):
|
||||
self.blink._auth_header = bad_header
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(self.blink._auth_header, bad_header)
|
||||
# pylint: disable=protected-access
|
||||
self.blink._summary_request()
|
||||
self.blink.summary_request()
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(self.blink._auth_header, original_header)
|
||||
|
||||
@@ -9,6 +9,9 @@ Blink system is set up.
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from blinkpy import blinkpy
|
||||
from blinkpy.helpers.util import BlinkURLHandler
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.camera import BlinkCamera
|
||||
from blinkpy.helpers.constants import BLINK_URL
|
||||
import tests.mock_responses as mresp
|
||||
|
||||
@@ -47,29 +50,34 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
'region_id': 'test'
|
||||
}
|
||||
|
||||
self.blink.urls = blinkpy.BlinkURLHandler('test')
|
||||
header = {
|
||||
'Host': 'abc.zxc',
|
||||
'TOKEN_AUTH': mresp.LOGIN_RESPONSE['authtoken']['authtoken']
|
||||
}
|
||||
self.blink.urls = BlinkURLHandler('test')
|
||||
self.blink.network_id = '0000'
|
||||
self.sync = BlinkSyncModule(self.blink, header, self.blink.urls)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after test."""
|
||||
self.blink = None
|
||||
|
||||
@mock.patch('blinkpy.blinkpy.Blink.camera_config_request',
|
||||
@mock.patch('blinkpy.sync_module.BlinkSyncModule.camera_config_request',
|
||||
return_value=CAMERA_CFG)
|
||||
@mock.patch('blinkpy.blinkpy.requests.post',
|
||||
@mock.patch('blinkpy.helpers.util.requests.post',
|
||||
side_effect=mresp.mocked_requests_post)
|
||||
@mock.patch('blinkpy.blinkpy.requests.get',
|
||||
@mock.patch('blinkpy.helpers.util.requests.get',
|
||||
side_effect=mresp.mocked_requests_get)
|
||||
def test_camera_properties(self, mock_get, mock_post, mock_cfg):
|
||||
"""Tests all property set/recall."""
|
||||
self.blink.urls = blinkpy.BlinkURLHandler('test')
|
||||
self.blink.urls = BlinkURLHandler('test')
|
||||
|
||||
self.blink.cameras = {
|
||||
'foobar': blinkpy.BlinkCamera(self.camera_config, self.blink)
|
||||
self.sync.cameras = {
|
||||
'foobar': BlinkCamera(self.camera_config, self.sync)
|
||||
}
|
||||
|
||||
for name in self.blink.cameras:
|
||||
camera = self.blink.cameras[name]
|
||||
for name in self.sync.cameras:
|
||||
camera = self.sync.cameras[name]
|
||||
camera.update(self.camera_config, skip_cache=True)
|
||||
self.assertEqual(camera.id, '1111')
|
||||
self.assertEqual(camera.name, 'foobar')
|
||||
@@ -98,8 +106,8 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
camera_config['temp'] = 60
|
||||
camera_config['battery'] = 0
|
||||
camera_config['notifications'] = 4
|
||||
for name in self.blink.cameras:
|
||||
camera = self.blink.cameras[name]
|
||||
for name in self.sync.cameras:
|
||||
camera = self.sync.cameras[name]
|
||||
camera.update(camera_config, skip_cache=True)
|
||||
self.assertEqual(camera.armed, True)
|
||||
self.assertEqual(
|
||||
@@ -120,22 +128,22 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
|
||||
def test_camera_case(self):
|
||||
"""Tests camera case sensitivity."""
|
||||
camera_object = blinkpy.BlinkCamera(self.camera_config, self.blink)
|
||||
self.blink.cameras['foobar'] = camera_object
|
||||
self.assertEqual(camera_object, self.blink.cameras['fOoBaR'])
|
||||
camera_object = BlinkCamera(self.camera_config, self.sync)
|
||||
self.sync.cameras['foobar'] = camera_object
|
||||
self.assertEqual(camera_object, self.sync.cameras['fOoBaR'])
|
||||
|
||||
@mock.patch('blinkpy.blinkpy.Blink.camera_config_request',
|
||||
@mock.patch('blinkpy.sync_module.BlinkSyncModule.camera_config_request',
|
||||
return_value=CAMERA_CFG)
|
||||
def test_camera_attributes(self, mock_cfg):
|
||||
"""Tests camera attributes."""
|
||||
self.blink.urls = blinkpy.BlinkURLHandler('test')
|
||||
self.blink.urls = BlinkURLHandler('test')
|
||||
|
||||
self.blink.cameras = {
|
||||
'foobar': blinkpy.BlinkCamera(self.camera_config, self.blink)
|
||||
self.sync.cameras = {
|
||||
'foobar': BlinkCamera(self.camera_config, self.sync)
|
||||
}
|
||||
|
||||
for name in self.blink.cameras:
|
||||
camera = self.blink.cameras[name]
|
||||
for name in self.sync.cameras:
|
||||
camera = self.sync.cameras[name]
|
||||
camera.update(self.camera_config, skip_cache=True)
|
||||
camera_attr = camera.attributes
|
||||
self.assertEqual(camera_attr['device_id'], '1111')
|
||||
@@ -156,3 +164,55 @@ class TestBlinkCameraSetup(unittest.TestCase):
|
||||
self.assertEqual(camera_attr['network_id'], '0000')
|
||||
self.assertEqual(camera_attr['motion_enabled'], True)
|
||||
self.assertEqual(camera_attr['wifi_strength'], -30)
|
||||
|
||||
@mock.patch('blinkpy.camera.BlinkCamera.image_refresh',
|
||||
side_effect='refresh/url')
|
||||
@mock.patch('blinkpy.helpers.util.requests.get',
|
||||
side_effect=mresp.mocked_requests_get)
|
||||
def test_camera_cache(self, req, img_refresh):
|
||||
"""Tests camera cache."""
|
||||
update_vals = {
|
||||
'name': 'foobar',
|
||||
'active': 'disabled',
|
||||
'video': '/clip.mp4',
|
||||
'thumbnail': '/image',
|
||||
'battery': 3,
|
||||
'notifications': 1,
|
||||
}
|
||||
self.sync.cameras = {
|
||||
'foobar': BlinkCamera(self.camera_config, self.sync)
|
||||
}
|
||||
|
||||
test_image = 'https://rest.test.immedia-semi.com/image.jpg'
|
||||
test_clip = 'https://rest.test.immedia-semi.com/clip.mp4'
|
||||
|
||||
for name, camera in self.sync.cameras.items():
|
||||
# Check that no cache returns None
|
||||
self.assertEqual(camera.name, name)
|
||||
self.assertEqual(camera.image_from_cache, None)
|
||||
self.assertEqual(camera.video_from_cache, None)
|
||||
|
||||
# Now, call an update with a new thumbnail to see if we update
|
||||
self.sync.records = []
|
||||
# pylint: disable=protected-access
|
||||
camera.update(update_vals)
|
||||
self.assertEqual(camera.thumbnail, test_image)
|
||||
self.assertEqual(camera.image_from_cache.status_code, 200)
|
||||
|
||||
# Now update the clip
|
||||
self.sync.record_dates = {camera.name: ['7', '1', '4', '3']}
|
||||
self.assertEqual(camera.last_record, list())
|
||||
camera.update(update_vals)
|
||||
self.assertEqual(camera.clip, test_clip)
|
||||
self.assertEqual(camera.last_record, list('7'))
|
||||
# First update should be false
|
||||
self.assertEqual(camera.motion_detected, False)
|
||||
self.sync.record_dates[camera.name].append('88')
|
||||
camera.update(update_vals)
|
||||
self.assertEqual(camera.last_record, ['88', '7'])
|
||||
self.assertEqual(camera.motion_detected, True)
|
||||
self.assertEqual(camera.video_from_cache.status_code, 200)
|
||||
# Next update shouldn't change records, and motion_dected=False
|
||||
camera.update(update_vals)
|
||||
self.assertEqual(camera.motion_detected, False)
|
||||
self.assertEqual(camera.video_from_cache.status_code, 200)
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Tests camera and system functions."""
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from blinkpy import blinkpy
|
||||
from blinkpy.sync_module import BlinkSyncModule
|
||||
from blinkpy.camera import BlinkCamera
|
||||
|
||||
USERNAME = 'foobar'
|
||||
PASSWORD = 'deadbeef'
|
||||
|
||||
|
||||
class MockSyncModule(BlinkSyncModule):
|
||||
"""Mock http requests from sync module."""
|
||||
|
||||
def __init__(self, blink, header):
|
||||
"""Create mock sync module instance."""
|
||||
super().__init__(blink, header)
|
||||
self.blink = blink
|
||||
self.header = header
|
||||
self.return_value = None
|
||||
self.return_value2 = None
|
||||
|
||||
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 TestBlinkSyncModule(unittest.TestCase):
|
||||
"""Test BlinkSyncModule functions in blinkpy."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up Blink module."""
|
||||
self.blink = blinkpy.Blink(username=USERNAME,
|
||||
password=PASSWORD)
|
||||
# pylint: disable=protected-access
|
||||
self.blink._auth_header = {
|
||||
'Host': 'test.url.tld',
|
||||
'TOKEN_AUTH': 'foobar123'
|
||||
}
|
||||
self.blink.urls = blinkpy.BlinkURLHandler('test')
|
||||
self.config = {
|
||||
'device_id': 1111,
|
||||
'name': 'foobar',
|
||||
'armed': False,
|
||||
'active': 'disabled',
|
||||
'thumbnail': '/test',
|
||||
'video': '/test.mp4',
|
||||
'temp': 80,
|
||||
'battery': 3,
|
||||
'notifications': 2,
|
||||
'region_id': 'test',
|
||||
'device_type': 'camera'
|
||||
}
|
||||
self.blink.sync = MockSyncModule(
|
||||
self.blink, self.blink._auth_header)
|
||||
|
||||
self.camera = BlinkCamera(self.config, self.blink.sync)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after test."""
|
||||
self.blink = None
|
||||
self.config = {}
|
||||
self.camera = None
|
||||
|
||||
def test_get_videos(self):
|
||||
"""Test video access."""
|
||||
self.blink.sync.return_value = [
|
||||
{
|
||||
'camera_name': 'foobar',
|
||||
'address': '/new/test.mp4',
|
||||
'thumbnail': '/test/thumb'
|
||||
}
|
||||
]
|
||||
self.blink.sync.get_videos()
|
||||
self.assertEqual(self.blink.sync.videos['foobar'][0]['clip'],
|
||||
'/new/test.mp4')
|
||||
self.assertEqual(self.blink.sync.videos['foobar'][0]['thumb'],
|
||||
'/test/thumb')
|
||||
|
||||
@mock.patch('blinkpy.sync_module.BlinkSyncModule.refresh')
|
||||
@mock.patch('blinkpy.sync_module.BlinkSyncModule._summary_request')
|
||||
@mock.patch('blinkpy.sync_module.BlinkSyncModule._video_request')
|
||||
def test_get_cameras(self, vid_req, req, refresh):
|
||||
"""Test camera extraction."""
|
||||
refresh.return_value = True
|
||||
req.return_value = {'devices': [self.config]}
|
||||
vid_req.return_value = [
|
||||
{
|
||||
'camera_name': 'foobar',
|
||||
'address': '/new.mp4',
|
||||
'thumbnail': '/new'
|
||||
}
|
||||
]
|
||||
self.blink.sync.get_cameras()
|
||||
self.assertTrue('foobar' in self.blink.sync.cameras)
|
||||
|
||||
def test_video_count(self):
|
||||
"""Test video count function."""
|
||||
self.blink.sync.return_value = {'count': 1}
|
||||
self.assertEqual(self.blink.sync.video_count, 1)
|
||||
Reference in New Issue
Block a user