Skip to content

Commit 47faf5a

Browse files
authored
Merge pull request #650 from perdue/perdue/feature/add_local_storage_api_support
Add support for the local storage API
2 parents ac0deea + cb1aca8 commit 47faf5a

14 files changed

+839
-45
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ docs/_build
1515
*.log
1616
venv
1717
.session*
18+
Pipfile
19+
Pipfile.lock

README.rst

+39
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,45 @@ Example usage, which downloads all videos recorded since July 4th, 2018 at 9:34a
183183
blink.download_videos('/home/blink', since='2018/07/04 09:34', delay=2)
184184
185185
186+
Sync Module Local Storage
187+
=========================
188+
189+
Description of how I think the local storage API is used by Blink
190+
-----------------------------------------------------------------
191+
192+
Since local storage is within a customer's residence, there are no guarantees for latency
193+
and availability. As a result, the API seems to be built to deal with these conditions.
194+
195+
In general, the approach appears to be this: The Blink app has to query the sync
196+
module for all information regarding the stored clips. On a click to view a clip, the app asks
197+
for the full list of stored clips, finds the clip in question, uploads the clip to the
198+
cloud, and then downloads the clip back from a cloud URL. Each interaction requires polling for
199+
the response since networking conditions are uncertain. The app also caches recent clips and the manifest.
200+
201+
API steps
202+
---------
203+
1. Request the local storage manifest be created by the sync module.
204+
205+
* POST **{base_url}/api/v1/accounts/{account_id}/networks/{network_id}/sync_modules/{sync_id}/local_storage/manifest/request**
206+
* Returns an ID that is used to get the manifest.
207+
208+
2. Retrieve the local storage manifest.
209+
210+
* GET **{base_url}/api/v1/accounts/{account_id}/networks/{network_id}/sync_modules/{sync_id}/local_storage/manifest/request/{manifest_request_id}**
211+
* Returns full manifest.
212+
* Extract the manifest ID from the response.
213+
214+
3. Find a clip ID in the clips list from the manifest to retrieve, and request an upload.
215+
216+
* POST **{base_url}/api/v1/accounts/{account_id}/networks/{network_id}/sync_modules/{sync_id}/local_storage/manifest/{manifest_id}/clip/request/{clip_id}**
217+
* When the response is returned, the upload has finished.
218+
219+
4. Download the clip using the same clip ID.
220+
221+
* GET **{base_url}/api/v1/accounts/{account_id}/networks/{network_id}/sync_modules/{sync_id}/local_storage/manifest/{manifest_id}/clip/request/{clip_id}**
222+
223+
224+
186225
.. |Build Status| image:: https://github.com/fronzbot/blinkpy/workflows/build/badge.svg
187226
:target: https://github.com/fronzbot/blinkpy/actions?query=workflow%3Abuild
188227
.. |Coverage Status| image:: https://codecov.io/gh/fronzbot/blinkpy/branch/dev/graph/badge.svg

blinkpy/api.py

+60-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
"""Implements known blink API calls."""
22

33
import logging
4+
import string
45
from json import dumps
5-
from blinkpy.helpers.util import get_time, Throttle
6+
from blinkpy.helpers.util import (
7+
get_time,
8+
Throttle,
9+
local_storage_clip_url_template,
10+
)
611
from blinkpy.helpers.constants import DEFAULT_URL, TIMEOUT, DEFAULT_USER_AGENT
712

813
_LOGGER = logging.getLogger(__name__)
@@ -21,7 +26,8 @@ def request_login(
2126
2227
:param auth: Auth instance.
2328
:param url: Login url.
24-
:login_data: Dictionary containing blink login data.
29+
:param login_data: Dictionary containing blink login data.
30+
:param is_retry:
2531
"""
2632
headers = {
2733
"Host": DEFAULT_URL,
@@ -296,9 +302,58 @@ def request_motion_detection_disable(blink, network, camera_id):
296302
return http_post(blink, url)
297303

298304

299-
def http_get(blink, url, stream=False, json=True, is_retry=False, timeout=TIMEOUT):
305+
def request_local_storage_manifest(blink, network, sync_id):
306+
"""Request creation of an updated manifest of video clips stored in sync module local storage.
307+
308+
:param blink: Blink instance.
309+
:param network: Sync module network id.
310+
:param sync_id: ID of sync module.
300311
"""
301-
Perform an http get request.
312+
url = (
313+
f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}/networks/{network}/sync_modules/{sync_id}"
314+
+ "/local_storage/manifest/request"
315+
)
316+
return http_post(blink, url)
317+
318+
319+
def get_local_storage_manifest(blink, network, sync_id, manifest_request_id):
320+
"""Request manifest of video clips stored in sync module local storage.
321+
322+
:param blink: Blink instance.
323+
:param network: Sync module network id.
324+
:param sync_id: ID of sync module.
325+
:param manifest_request_id: Request ID of local storage manifest (requested creation of new manifest).
326+
"""
327+
url = (
328+
f"{blink.urls.base_url}/api/v1/accounts/{blink.account_id}/networks/{network}/sync_modules/{sync_id}"
329+
+ f"/local_storage/manifest/request/{manifest_request_id}"
330+
)
331+
return http_get(blink, url)
332+
333+
334+
def request_local_storage_clip(blink, network, sync_id, manifest_id, clip_id):
335+
"""Prepare video clip stored in the sync module to be downloaded.
336+
337+
:param blink: Blink instance.
338+
:param network: Sync module network id.
339+
:param sync_id: ID of sync module.
340+
:param manifest_id: ID of local storage manifest (returned in the manifest response).
341+
:param clip_id: ID of the clip.
342+
"""
343+
url = blink.urls.base_url + string.Template(
344+
local_storage_clip_url_template()
345+
).substitute(
346+
account_id=blink.account_id,
347+
network_id=network,
348+
sync_id=sync_id,
349+
manifest_id=manifest_id,
350+
clip_id=clip_id,
351+
)
352+
return http_post(blink, url)
353+
354+
355+
def http_get(blink, url, stream=False, json=True, is_retry=False, timeout=TIMEOUT):
356+
"""Perform an http get request.
302357
303358
:param url: URL to perform get request.
304359
:param stream: Stream response? True/FALSE
@@ -317,8 +372,7 @@ def http_get(blink, url, stream=False, json=True, is_retry=False, timeout=TIMEOU
317372

318373

319374
def http_post(blink, url, is_retry=False, data=None, json=True, timeout=TIMEOUT):
320-
"""
321-
Perform an http post request.
375+
"""Perform an http post request.
322376
323377
:param url: URL to perfom post request.
324378
:param is_retry: Is this part of a re-auth attempt?

blinkpy/blinkpy.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import os.path
1717
import time
1818
import logging
19+
import datetime
1920
from shutil import copyfileobj
2021

2122
from requests.structures import CaseInsensitiveDict
@@ -88,12 +89,17 @@ def refresh(self, force=False, force_cache=False):
8889
self.setup_post_verify()
8990

9091
self.get_homescreen()
92+
9193
for sync_name, sync_module in self.sync.items():
92-
_LOGGER.debug("Attempting refresh of sync %s", sync_name)
94+
_LOGGER.debug("Attempting refresh of blink.sync['%s']", sync_name)
9395
sync_module.refresh(force_cache=(force or force_cache))
96+
9497
if not force_cache:
9598
# Prevents rapid clearing of motion detect property
9699
self.last_refresh = int(time.time())
100+
last_refresh = datetime.datetime.fromtimestamp(self.last_refresh)
101+
_LOGGER.debug(f"last_refresh={last_refresh}")
102+
97103
return True
98104
return False
99105

@@ -114,6 +120,15 @@ def start(self):
114120
if self.auth.no_prompt:
115121
return True
116122
self.setup_prompt_2fa()
123+
124+
if not self.last_refresh:
125+
# Initialize last_refresh to be just before the refresh delay period.
126+
self.last_refresh = int(time.time() - self.refresh_rate * 1.05)
127+
_LOGGER.debug(
128+
f"Initialized last_refresh to {self.last_refresh} == "
129+
+ f"{datetime.datetime.fromtimestamp(self.last_refresh)}"
130+
)
131+
117132
return self.setup_post_verify()
118133

119134
def setup_prompt_2fa(self):

0 commit comments

Comments
 (0)