+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+"""
+blinkpy by Kevin Fronczak - A Blink camera Python library.
+
+https://github.com/fronzbot/blinkpy
+Original protocol hacking by MattTW :
+https://github.com/MattTW/BlinkMonitorProtocol
+Published under the MIT license - See LICENSE file for more details.
+"Blink Wire-Free HS Home Monitoring & Alert Systems" is a trademark
+owned by Immedia Inc., see www.blinkforhome.com for more information.
+I am in no way affiliated with Blink, nor Immedia Inc.
+"""
+
+import json
+import getpass
+from shutil import copyfileobj
+import requests
+from requests.structures import CaseInsensitiveDict
+import blinkpy.helpers.errors as ERROR
+from blinkpy.helpers.constants import (
+ BLINK_URL, LOGIN_URL, LOGIN_BACKUP_URL,
+ DEFAULT_URL, ONLINE
+)
+
+
+[docs]def _attempt_reauthorization(blink):
+
"""Attempt to refresh auth token and links."""
+
headers = blink.get_auth_token()
+
blink.set_links()
+
return headers
+
+
+[docs]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
+
+
+[docs]class BlinkURLHandler(object):
+
"""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)
+
+
+[docs]class BlinkCamera(object):
+
"""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['armed']
+
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 = config['battery']
+
self.notifications = config['notifications']
+
self.motion = {}
+
self.header = None
+
self.image_link = None
+
self.arm_link = None
+
self.region_id = config['region_id']
+
+
@property
+
def armed(self):
+
"""Return camera arm status."""
+
return self._status
+
+
@property
+
def battery_string(self):
+
"""Return string indicating battery status."""
+
status = "Unknown"
+
if self.battery > 1 and self.battery <= 3:
+
status = "OK"
+
elif self.battery >= 0:
+
status = "Low"
+
return status
+
+
[docs] 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')
+
+
[docs] 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')
+
+
[docs] def update(self, values):
+
"""Update camera information."""
+
self.name = values['name']
+
self._status = values['armed']
+
self.thumbnail = "{}{}.jpg".format(
+
self.urls.base_url, values['thumbnail'])
+
self.clip = "{}{}".format(
+
self.urls.base_url, values['video'])
+
self.temperature = values['temp']
+
self.battery = values['battery']
+
self.notifications = values['notifications']
+
+
[docs] 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
+
+
[docs] def image_to_file(self, path):
+
"""Write image to file."""
+
thumb = self.image_refresh()
+
response = _request(self.blink, url=thumb, headers=self.header,
+
reqtype='get', stream=True, json_resp=False)
+
if response.status_code == 200:
+
with open(path, 'wb') as imgfile:
+
copyfileobj(response.raw, imgfile)
+
+
[docs] def video_to_file(self, path):
+
"""Write video to file."""
+
response = _request(self.blink, url=self.clip, headers=self.header,
+
reqtype='get', stream=True, json_resp=False)
+
with open(path, 'wb') as vidfile:
+
copyfileobj(response.raw, vidfile)
+
+
+[docs]class Blink(object):
+
"""Class to initialize communication and sync module."""
+
+
def __init__(self, username=None, password=None):
+
"""Initialize Blink system."""
+
self._username = username
+
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.urls = None
+
self._video_count = 0
+
self._all_videos = {}
+
self._summary = 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
+
+
@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')
+
+
[docs] def refresh(self):
+
"""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']
+
element['thumbnail'] = self.videos[name][0]['thumb']
+
camera.update(element)
+
except KeyError:
+
pass
+
return None
+
+
[docs] def get_videos(self, start_page=0, end_page=1):
+
"""Retrieve last recorded videos per camera."""
+
videos = list()
+
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:
+
for entry in page:
+
camera_name = entry['camera_name']
+
clip_addr = entry['address']
+
thumb_addr = entry['thumbnail']
+
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,
+
}
+
]
+
+
[docs] 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
+
+
[docs] 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
+
+
[docs] def setup_system(self):
+
"""
+
Perform full system setup.
+
+
Method logs in and sets auth token, urls, and ids for future requests.
+
Essentially this is just a wrapper function for ease of use.
+
"""
+
if self._username is None or self._password is None:
+
raise BlinkAuthenticationException(ERROR.AUTHENTICATE)
+
+
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()
+
+
[docs] def login(self):
+
"""Prompt user for username and password."""
+
self._username = input("Username:")
+
self._password = getpass.getpass("Password:")
+
+
[docs] def get_auth_token(self):
+
"""Retrieve the authentication token from Blink."""
+
if not isinstance(self._username, str):
+
raise BlinkAuthenticationException(ERROR.USERNAME)
+
if not isinstance(self._password, str):
+
raise BlinkAuthenticationException(ERROR.PASSWORD)
+
+
headers = {'Host': DEFAULT_URL,
+
'Content-Type': 'application/json'}
+
data = json.dumps({
+
"email": self._username,
+
"password": self._password,
+
"client_specifier": "iPhone 9.2 | 2.2 | 222"
+
})
+
response = _request(self, url=LOGIN_URL, headers=headers,
+
data=data, json_resp=False, reqtype='post')
+
if response.status_code == 200:
+
response = response.json()
+
(self.region_id, self.region), = response['region'].items()
+
else:
+
response = _request(self, url=LOGIN_BACKUP_URL, headers=headers,
+
data=data, reqtype='post')
+
self.region_id = 'piri'
+
self.region = "UNKNOWN"
+
+
self._host = "{}.{}".format(self.region_id, BLINK_URL)
+
self._token = response['authtoken']['authtoken']
+
+
self._auth_header = {'Host': self._host,
+
'TOKEN_AUTH': self._token}
+
+
self.urls = BlinkURLHandler(self.region_id)
+
+
return self._auth_header
+
+
[docs] def get_ids(self):
+
"""Set the network ID and Account ID."""
+
response = self._network_request()
+
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')
+
+
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')
+
+
def _status_request(self):
+
"""Get syncmodule status."""
+
url = "{}/{}/syncmodules".format(self.urls.network_url,
+
self.network_id)
+
headers = self._auth_header
+
return _request(self, url=url, headers=headers, reqtype='get')
+