diff --git a/blinkpy/api.py b/blinkpy/api.py index 62f66b1..665abcd 100644 --- a/blinkpy/api.py +++ b/blinkpy/api.py @@ -2,7 +2,8 @@ import logging from json import dumps -from blinkpy.helpers.util import http_req +import blinkpy.helpers.errors as ERROR +from blinkpy.helpers.util import http_req, BlinkException from blinkpy.helpers.constants import DEFAULT_URL _LOGGER = logging.getLogger(__name__) @@ -31,7 +32,7 @@ def request_networks(blink): def request_syncmodule(blink, network): """Request sync module info.""" - url = "{}/network/{}/syncmodule".format(blink.urls.base_url, network) + url = "{}/network/{}/syncmodules".format(blink.urls.base_url, network) return http_get(blink, url) @@ -41,7 +42,7 @@ def request_system_arm(blink, network): return http_post(blink, url) -def request_system_disarm(blink, headers, network): +def request_system_disarm(blink, network): """Disarm system.""" url = "{}/network/{}/disarm".format(blink.urls.base_url, network) return http_post(blink, url) @@ -141,6 +142,8 @@ def http_get(blink, url, stream=False, json=True): :param stream: Stream response? True/FALSE :param json: Return json response? TRUE/False """ + if blink.auth_header is None: + raise BlinkException(ERROR.AUTH_TOKEN) return http_req(blink, url=url, headers=blink.auth_header, reqtype='get', stream=stream, json_resp=json) @@ -151,4 +154,6 @@ def http_post(blink, url): :param url: URL to perfom post request. """ + if blink.auth_header is None: + raise BlinkException(ERROR.AUTH_TOKEN) return http_req(blink, url=url, headers=blink.auth_header, reqtype='post') diff --git a/blinkpy/blinkpy.py b/blinkpy/blinkpy.py index 65b2b2c..f9aafa7 100644 --- a/blinkpy/blinkpy.py +++ b/blinkpy/blinkpy.py @@ -1,4 +1,3 @@ - # -*- coding: utf-8 -*- """ blinkpy by Kevin Fronczak - A Blink camera Python library. @@ -13,16 +12,16 @@ I am in no way affiliated with Blink, nor Immedia Inc. """ import time -import json import getpass import logging import blinkpy.helpers.errors as ERROR +from blinkpy import api from blinkpy.sync_module import BlinkSyncModule from blinkpy.helpers.util import ( - http_req, create_session, BlinkURLHandler, - BlinkException, BlinkAuthenticationException) + create_session, BlinkURLHandler, + BlinkAuthenticationException) from blinkpy.helpers.constants import ( - DEFAULT_URL, BLINK_URL, LOGIN_URL, LOGIN_BACKUP_URL) + BLINK_URL, LOGIN_URL, LOGIN_BACKUP_URL) REFRESH_RATE = 30 @@ -47,9 +46,6 @@ class Blink(): self._token = None self._auth_header = None self._host = None - self._events = [] - self._last_summary = None - self._last_events = None self.network_id = None self.account_id = None self.urls = None @@ -62,9 +58,9 @@ class Blink(): self._login_url = LOGIN_URL @property - def events(self): - """Get all events on server.""" - return self._events + def auth_header(self): + """Return the authentication header.""" + return self._auth_header def start(self): """ @@ -79,12 +75,7 @@ class Blink(): self.get_auth_token() self.get_ids() - 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() + self.sync = BlinkSyncModule(self) def login(self): """Prompt user for username and password.""" @@ -103,16 +94,13 @@ class Blink(): 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" - }) + login_url = LOGIN_URL self.session = create_session() - response = http_req(self, url=self._login_url, headers=headers, - data=data, json_resp=False, reqtype='post') + response = api.request_login(self, + login_url, + self._username, + self._password) + if response.status_code == 200: response = response.json() (self.region_id, self.region), = response['region'].items() @@ -122,57 +110,30 @@ class Blink(): "when authenticating, " "trying new url"), response.status_code ) - self._login_url = LOGIN_BACKUP_URL - response = http_req(self, url=self._login_url, headers=headers, - data=data, reqtype='post') + login_url = LOGIN_BACKUP_URL + response = api.request_login(self, + login_url, + self._username, + self._password) 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) + self._login_url = login_url return self._auth_header def get_ids(self): """Set the network ID and Account ID.""" - response = self._network_request() + response = api.request_networks(self) self.network_id = str(response['networks'][0]['id']) self.account_id = str(response['networks'][0]['account_id']) - 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 http_req(self, url=url, headers=headers, reqtype='get') - - def events_request(self, skip_throttle=False): - """Get events on server.""" - url = "{}/{}".format(self.urls.event_url, self.network_id) - headers = self._auth_header - if self.check_if_ok_to_update() or skip_throttle: - self._last_events = http_req(self, url=url, - headers=headers, - reqtype='get') - return self._last_events - - def summary_request(self, skip_throttle=False): - """Get blink summary.""" - url = self.urls.home_url - headers = self._auth_header - if headers is None: - raise BlinkException(ERROR.AUTH_TOKEN) - if self.check_if_ok_to_update() or skip_throttle: - self._last_summary = http_req( - self, url=url, headers=headers, reqtype='get') - return self._last_summary - def refresh(self, force_cache=False): """ Perform a system refresh. @@ -181,8 +142,6 @@ class Blink(): """ if self.check_if_ok_to_update() or force_cache: _LOGGER.debug("Attempting refresh of cameras.") - self._last_events = self.events_request(skip_throttle=True) - self._last_summary = self.summary_request(skip_throttle=True) self.sync.refresh(force_cache=force_cache) def check_if_ok_to_update(self): diff --git a/blinkpy/camera.py b/blinkpy/camera.py index 6e95a20..c02a220 100644 --- a/blinkpy/camera.py +++ b/blinkpy/camera.py @@ -2,7 +2,7 @@ from shutil import copyfileobj import logging -from requests.exceptions import RequestException +from blinkpy import api _LOGGER = logging.getLogger(__name__) @@ -12,30 +12,22 @@ MAX_CLIPS = 5 class BlinkCamera(): """Class to initialize individual camera.""" - def __init__(self, config, sync): + def __init__(self, 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.name = None + self.camera_id = None + self.network_id = None + self.thumbnail = None + self.serial = None + self.motion_enabled = None + self.battery_voltage = None + self.clip = None + self.temperature = None + self.battery_state = None self.motion_detected = None self.wifi_strength = None - self.camera_config = dict() - self.motion_enabled = None - self.last_record = list() + self.last_record = [] self._cached_image = None self._cached_video = None @@ -44,16 +36,14 @@ class BlinkCamera(): """Return dictionary of all camera attributes.""" attributes = { 'name': self.name, - 'device_id': self.id, - 'status': self._status, - 'armed': self.armed, + 'camera_id': self.camera_id, + 'serial': self.serial, '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, @@ -61,31 +51,11 @@ class BlinkCamera(): } 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.""" @@ -107,50 +77,63 @@ class BlinkCamera(): def snap_picture(self): """Take a picture with camera to create a new thumbnail.""" - self.sync.http_post(self.image_link) + return api.request_new_image(self.sync.blink, + self.network_id, + self.camera_id) 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')) + return api.request_motion_detection_enable(self.sync.blink, + self.network_id, + self.camera_id) + return api.request_motion_detection_disable(self.sync.blink, + self.network_id, + self.camera_id) - 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'] + def update(self, config, force_cache=False): + """Update camera info.""" + self.name = config['name'] + self.camera_id = str(config['camera_id']) + self.network_id = str(config['network_id']) + self.serial = config['serial'] + self.motion_enabled = config['enabled'] + self.battery_voltage = config['battery_voltage'] + self.battery_state = config['battery_state'] + self.temperature = config['temperature'] + self.wifi_strength = config['wifi_strength'] + if config['thumbnail']: + new_thumbnail = "{}{}.jpg".format(self.sync.urls.base_url, + config['thumbnail']) + + self.check_for_motion() + if self.last_record: + clip_addr = self.sync.all_clips[self.name][self.last_record[0]] + self.clip = "{}{}".format(self.sync.urls.base_url, + clip_addr) 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) + update_cached_video = False + if self._cached_video is None or self.motion_detected: + update_cached_video = True - # Check if the most recent clip is included in the last_record list - # and that the last_record list is populated + if update_cached_image or force_cache: + self._cached_image = api.http_get(self.sync.blink, + url=self.thumbnail, + stream=True, + json=False) + if update_cached_video or force_cache: + self._cached_video = api.http_get(self.sync.blink, + url=self.clip, + stream=True, + json=False) + + def check_for_motion(self): + """Check if motion detected..""" try: records = sorted(self.sync.record_dates[self.name]) new_clip = records.pop() @@ -168,30 +151,6 @@ class BlinkCamera(): _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. diff --git a/blinkpy/helpers/util.py b/blinkpy/helpers/util.py index 481547f..f6c16a9 100644 --- a/blinkpy/helpers/util.py +++ b/blinkpy/helpers/util.py @@ -19,7 +19,6 @@ 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 diff --git a/blinkpy/sync_module.py b/blinkpy/sync_module.py index d47d16a..93e4c28 100644 --- a/blinkpy/sync_module.py +++ b/blinkpy/sync_module.py @@ -3,8 +3,8 @@ import logging from requests.structures import CaseInsensitiveDict +from blinkpy import api from blinkpy.camera import BlinkCamera -from blinkpy.helpers.util import http_req from blinkpy.helpers.constants import ONLINE _LOGGER = logging.getLogger(__name__) @@ -13,117 +13,112 @@ _LOGGER = logging.getLogger(__name__) class BlinkSyncModule(): """Class to initialize sync module.""" - def __init__(self, blink, header, urls=None): + def __init__(self, blink): """ Initialize Blink sync module. :param blink: Blink class instantiation - :param header: Blink authentication header - :param urls: URL Handler instantiation (deprecated) """ self.blink = blink - self._auth_header = header + self._auth_header = blink.auth_header + self.network_id = blink.network_id + self.region = blink.region + self.region_id = blink.region_id + self.name = 'sync' + self.serial = None + self.status = None self.sync_id = None - self.region = None - self.region_id = None - self._host = None - self._events = [] + self.host = None + self.summary = None + self.homescreen = None + self.record_dates = {} + self.videos = {} + 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 + self.all_clips = {} + + self.start() + + @property + def attributes(self): + """Return sync attributes.""" + attr = { + 'name': self.name, + 'id': self.sync_id, + 'network_id': self.network_id, + 'serial': self.serial, + 'status': self.status, + 'region': self.region, + 'region_id': self.region_id, + } + return attr @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 + return ONLINE[self.status] @property def arm(self): """Return status of sync module: armed/disarmed.""" - return self._summary['network']['armed'] + return self.homescreen['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) + return api.request_system_arm(self.blink, self.network_id) + + return api.request_system_disarm(self.blink, self.network_id) + + def start(self): + """Initialize the system.""" + response = api.request_syncmodule(self.blink, self.network_id) + self.summary = response['syncmodule'] + self.name = self.summary['name'] + self.sync_id = self.summary['id'] + self.network_id = self.summary['network_id'] + self.serial = self.summary['serial'] + self.status = self.summary['status'] + + self.events = self.get_events() + + self.homescreen = api.request_homescreen(self.blink, self.network_id) + + camera_info = self.get_camera_info() + for camera_config in camera_info: + name = camera_config['name'] + self.cameras[name] = BlinkCamera(self) + + self.videos = self.get_videos() + for camera_config in camera_info: + name = camera_config['name'] + self.cameras[name].update(camera_config, force_cache=True) + + def get_events(self): + """Retrieve events from server.""" + response = api.request_sync_events(self.blink, self.network_id) + return response['event'] + + def get_camera_info(self): + """Retrieve camera information.""" + response = api.request_cameras(self.blink, self.network_id) + return response['devicestatus'] def refresh(self, force_cache=False): """Get all blink cameras and pulls their most recent status.""" - summary = self._summary_request() - events = self._events_request() - response = 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._summary = summary - self._events = events + self.events = self.get_events() + self.videos = self.get_videos() + self.homescreen = api.request_homescreen(self.blink, self.network_id) + camera_info = self.get_camera_info() + for camera_config in camera_info: + name = camera_config['name'] + self.cameras[name].update(camera_config, force_cache=force_cache) def get_videos(self, start_page=0, end_page=1): """ @@ -135,8 +130,11 @@ class BlinkSyncModule(): """ videos = list() all_dates = dict() + for camera in self.cameras: + self.all_clips[camera] = {} + for page_num in range(start_page, end_page + 1): - this_page = self._video_request(page_num) + this_page = api.request_videos(self.blink, page=page_num) if not this_page: break videos.append(this_page) @@ -150,18 +148,19 @@ class BlinkSyncModule(): clip_date = clip_addr.split('_')[-6:] clip_date = '_'.join(clip_date) clip_date = clip_date.split('.')[0] + self.all_clips[camera_name][clip_date] = clip_addr 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( + self.videos[camera_name].append( { 'clip': clip_addr, 'thumb': thumb_addr, } ) except KeyError: - self._all_videos[camera_name] = [ + self.videos[camera_name] = [ { 'clip': clip_addr, 'thumb': thumb_addr, @@ -169,83 +168,4 @@ class BlinkSyncModule(): ] 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 an http get request. - - :param url: URL to perform get request - :param stream: Stream response? True/FALSE - :param json: Return json response? TRUE/False - """ - 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. - - :param url: URL to perform post request - """ - return http_req(self.blink, url=url, headers=self._auth_header, - reqtype='post') + return self.videos