Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ced64de65 | ||
|
|
b41460c569 | ||
|
|
9ff02de48b | ||
|
|
14cbbacc1b | ||
|
|
1cadf3488d | ||
|
|
c78f411014 | ||
|
|
ab2db14bc8 | ||
|
|
36ff8e4917 | ||
|
|
f42c49d221 | ||
|
|
114f8fbfa8 | ||
|
|
8bbf03fdb4 | ||
|
|
5b57982b41 | ||
|
|
ecf7a5b13a | ||
|
|
f68951fab7 | ||
|
|
23eec5187d | ||
|
|
caa3e10648 | ||
|
|
4bc0b80eea | ||
|
|
959a7dfa85 | ||
|
|
9e793b6748 | ||
|
|
c75e057e83 | ||
|
|
37350da1b2 | ||
|
|
19b7eef123 | ||
|
|
6bd8d14190 | ||
|
|
f6ddfbe5cf | ||
|
|
1b66eddfb4 |
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.8]
|
||||
python-version: [3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@@ -14,3 +14,4 @@ build/*
|
||||
docs/_build
|
||||
*.log
|
||||
venv
|
||||
.session*
|
||||
|
||||
+23
@@ -4,6 +4,29 @@ Changelog
|
||||
|
||||
A list of changes between each release
|
||||
|
||||
0.17.0 (2021-02-15)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes:**
|
||||
- Fix video downloading bug (`#424 <https://github.com/fronzbot/blinkpy/pull/424>`__)
|
||||
- Fix repeated authorization email bug (`#432 <https://github.com/fronzbot/blinkpy/pull/432>`__ and `#428 <https://github.com/fronzbot/blinkpy/pull/428>`__)
|
||||
|
||||
**New Features:**
|
||||
- Add logout method (`#429 <https://github.com/fronzbot/blinkpy/pull/429>`__)
|
||||
- Add camera record method (`#430 <https://github.com/fronzbot/blinkpy/pull/430>`__)
|
||||
|
||||
**Other:**
|
||||
- Add debug script to main repo to help with general debug
|
||||
- Upgrade login endpoint from v4 to v5
|
||||
- Add python 3.9 support
|
||||
- Bump coverage to 5.4
|
||||
- Bump pytest to 6.2.2
|
||||
- Bump pytest-cov to 2.11.1
|
||||
- Bump pygments to 2.8.0
|
||||
- Bump pre-commit to 2.10.1
|
||||
- Bump restructuredtext-lint to 1.3.2
|
||||
|
||||
|
||||
0.16.4 (2020-11-22)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
+7
-3
@@ -23,18 +23,16 @@ def request_login(
|
||||
headers = {
|
||||
"Host": DEFAULT_URL,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "/",
|
||||
"user-agent": DEFAULT_USER_AGENT,
|
||||
}
|
||||
data = dumps(
|
||||
{
|
||||
"email": login_data["username"],
|
||||
"password": login_data["password"],
|
||||
"notification_key": login_data["notification_key"],
|
||||
"unique_id": login_data["uid"],
|
||||
"device_identifier": login_data["device_id"],
|
||||
"client_name": "Computer",
|
||||
"reauth": "false",
|
||||
"reauth": True,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -57,6 +55,12 @@ def request_verify(auth, blink, verify_key):
|
||||
)
|
||||
|
||||
|
||||
def request_logout(blink):
|
||||
"""Logout of blink servers."""
|
||||
url = f"{blink.urls.base_url}/api/v4/account/{blink.account_id}/client/{blink.client_id}/logout"
|
||||
return http_post(blink, url=url)
|
||||
|
||||
|
||||
def request_networks(blink):
|
||||
"""Request all networks information."""
|
||||
url = f"{blink.urls.base_url}/networks"
|
||||
|
||||
+18
-14
@@ -108,8 +108,12 @@ class Auth:
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
raise LoginError
|
||||
except AttributeError:
|
||||
raise LoginError
|
||||
except AttributeError as error:
|
||||
raise LoginError from error
|
||||
|
||||
def logout(self, blink):
|
||||
"""Log out."""
|
||||
return api.request_logout(blink)
|
||||
|
||||
def refresh_token(self):
|
||||
"""Refresh auth token."""
|
||||
@@ -119,21 +123,21 @@ class Auth:
|
||||
self.login_response = self.login()
|
||||
self.extract_login_info()
|
||||
self.is_errored = False
|
||||
except LoginError:
|
||||
except LoginError as error:
|
||||
_LOGGER.error("Login endpoint failed. Try again later.")
|
||||
raise TokenRefreshFailed
|
||||
except (TypeError, KeyError):
|
||||
raise TokenRefreshFailed from error
|
||||
except (TypeError, KeyError) as error:
|
||||
_LOGGER.error("Malformed login response: %s", self.login_response)
|
||||
raise TokenRefreshFailed
|
||||
raise TokenRefreshFailed from error
|
||||
return True
|
||||
|
||||
def extract_login_info(self):
|
||||
"""Extract login info from login response."""
|
||||
self.region_id = self.login_response["region"]["tier"]
|
||||
self.region_id = self.login_response["account"]["tier"]
|
||||
self.host = f"{self.region_id}.{BLINK_URL}"
|
||||
self.token = self.login_response["authtoken"]["authtoken"]
|
||||
self.client_id = self.login_response["client"]["id"]
|
||||
self.account_id = self.login_response["account"]["id"]
|
||||
self.token = self.login_response["auth"]["token"]
|
||||
self.client_id = self.login_response["account"]["client_id"]
|
||||
self.account_id = self.login_response["account"]["account_id"]
|
||||
|
||||
def startup(self):
|
||||
"""Initialize tokens for communication."""
|
||||
@@ -155,8 +159,8 @@ class Auth:
|
||||
json_data = response.json()
|
||||
except KeyError:
|
||||
pass
|
||||
except (AttributeError, ValueError):
|
||||
raise BlinkBadResponse
|
||||
except (AttributeError, ValueError) as error:
|
||||
raise BlinkBadResponse from error
|
||||
|
||||
self.is_errored = False
|
||||
return json_data
|
||||
@@ -232,7 +236,7 @@ class Auth:
|
||||
json_resp = response.json()
|
||||
blink.available = json_resp["valid"]
|
||||
if not json_resp["valid"]:
|
||||
_LOGGER.error(f"{json_resp['message']}")
|
||||
_LOGGER.error("%s", json_resp["message"])
|
||||
return False
|
||||
except (KeyError, TypeError):
|
||||
_LOGGER.error("Did not receive valid response from server.")
|
||||
@@ -242,7 +246,7 @@ class Auth:
|
||||
def check_key_required(self):
|
||||
"""Check if 2FA key is required."""
|
||||
try:
|
||||
if self.login_response["client"]["verification_required"]:
|
||||
if self.login_response["account"]["client_verification_required"]:
|
||||
return True
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
@@ -98,6 +98,10 @@ class BlinkCamera:
|
||||
self.sync.blink, self.network_id, self.camera_id
|
||||
)
|
||||
|
||||
def record(self):
|
||||
"""Initiate clip recording."""
|
||||
return api.request_new_video(self.sync.blink, self.network_id, self.camera_id)
|
||||
|
||||
def get_media(self, media_type="image"):
|
||||
"""Download media (image or video)."""
|
||||
url = self.thumbnail
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import os
|
||||
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 16
|
||||
PATCH_VERSION = "5.rc0"
|
||||
MINOR_VERSION = 17
|
||||
PATCH_VERSION = 0
|
||||
|
||||
__version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}.{PATCH_VERSION}"
|
||||
|
||||
@@ -49,7 +49,7 @@ URLS
|
||||
BLINK_URL = "immedia-semi.com"
|
||||
DEFAULT_URL = f"rest-prod.{BLINK_URL}"
|
||||
BASE_URL = f"https://{DEFAULT_URL}"
|
||||
LOGIN_ENDPOINT = f"{BASE_URL}/api/v4/account/login"
|
||||
LOGIN_ENDPOINT = f"{BASE_URL}/api/v5/account/login"
|
||||
|
||||
"""
|
||||
Dictionaries
|
||||
|
||||
@@ -33,10 +33,13 @@ def json_save(data, file_name):
|
||||
json.dump(data, json_file, indent=4)
|
||||
|
||||
|
||||
def gen_uid(size):
|
||||
def gen_uid(size, uid_format=False):
|
||||
"""Create a random sring."""
|
||||
full_token = secrets.token_hex(size)
|
||||
return full_token[0:size]
|
||||
if uid_format:
|
||||
token = f"BlinkCamera_{secrets.token_hex(4)}-{secrets.token_hex(2)}-{secrets.token_hex(2)}-{secrets.token_hex(2)}-{secrets.token_hex(6)}"
|
||||
else:
|
||||
token = secrets.token_hex(size)
|
||||
return token
|
||||
|
||||
|
||||
def time_to_seconds(timestamp):
|
||||
@@ -79,10 +82,7 @@ def prompt_login_data(data):
|
||||
|
||||
def validate_login_data(data):
|
||||
"""Check for missing keys."""
|
||||
data["uid"] = data.get("uid", gen_uid(const.SIZE_UID))
|
||||
data["notification_key"] = data.get(
|
||||
"notification_key", gen_uid(const.SIZE_NOTIFICATION_KEY)
|
||||
)
|
||||
data["uid"] = data.get("uid", gen_uid(const.SIZE_UID, uid_format=True))
|
||||
data["device_id"] = data.get("device_id", const.DEVICE_ID)
|
||||
|
||||
return data
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Login testing script."""
|
||||
import sys
|
||||
import os
|
||||
from blinkpy.blinkpy import Blink
|
||||
from blinkpy.auth import Auth
|
||||
from blinkpy.helpers.util import json_load, json_save
|
||||
|
||||
save_session = False
|
||||
print("")
|
||||
print("Blink Login Debug Script ...")
|
||||
print(" ... Loading previous session information.")
|
||||
cwd = os.getcwd()
|
||||
print(f" ... Looking in {cwd}.")
|
||||
session_path = os.path.join(cwd, ".session_debug")
|
||||
session = json_load(session_path)
|
||||
|
||||
try:
|
||||
auth_file = session["file"]
|
||||
except (TypeError, KeyError):
|
||||
print(" ... Please input location of auth file")
|
||||
auth_file = input(" Must contain username and password: ")
|
||||
save_session = True
|
||||
|
||||
data = json_load(auth_file)
|
||||
if data is None:
|
||||
print(f" ... Please fix file contents of {auth_file}.")
|
||||
print(" ... Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
username = data["username"]
|
||||
password = data["password"]
|
||||
except KeyError:
|
||||
print(f" ... File contents of {auth_file} incorrect.")
|
||||
print(" ... Require username and password at minimum.")
|
||||
print(" ... Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
if save_session:
|
||||
print(f" ... Saving session file to {session_path}.")
|
||||
json_save({"file": auth_file}, session_path)
|
||||
|
||||
blink = Blink()
|
||||
auth = Auth(data)
|
||||
blink.auth = auth
|
||||
|
||||
print(" ... Starting Blink.")
|
||||
print("")
|
||||
blink.start()
|
||||
print("")
|
||||
print(" ... Printing login response.")
|
||||
print("")
|
||||
print(blink.auth.login_response)
|
||||
print("")
|
||||
print(" ... Printing login attributes.")
|
||||
print("")
|
||||
print(blink.auth.login_attributes)
|
||||
print("")
|
||||
input(" ... Press any key to continue: ")
|
||||
print(" ... Deactivating auth token.")
|
||||
blink.auth.token = "foobar"
|
||||
print(f"\t - blink.auth.token = {blink.auth.token}")
|
||||
|
||||
print(" ... Attempting login.")
|
||||
print("")
|
||||
blink.start()
|
||||
print("")
|
||||
print(" ... Printing login response.")
|
||||
print("")
|
||||
print(blink.auth.login_response)
|
||||
print("")
|
||||
print(" ... Printing login attributes.")
|
||||
print("")
|
||||
print(blink.auth.login_attributes)
|
||||
print("")
|
||||
rint(" ... Done.")
|
||||
print("")
|
||||
@@ -2,7 +2,7 @@ black==19.10b0
|
||||
coverage==5.4
|
||||
flake8==3.8.4
|
||||
flake8-docstrings==1.5.0
|
||||
pre-commit==2.9.3
|
||||
pre-commit==2.10.1
|
||||
pylint==2.6.0
|
||||
pydocstyle==5.1.1
|
||||
pytest==6.2.2
|
||||
@@ -10,5 +10,5 @@ pytest-cov==2.11.1
|
||||
pytest-sugar==0.9.4
|
||||
pytest-timeout==1.4.2
|
||||
restructuredtext-lint==1.3.2
|
||||
pygments==2.7.4
|
||||
pygments==2.8.0
|
||||
testtools>=2.4.0
|
||||
|
||||
+11
-8
@@ -42,7 +42,6 @@ class TestAuth(unittest.TestCase):
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"uid": 1234,
|
||||
"notification_key": 1234,
|
||||
"device_id": const.DEVICE_ID,
|
||||
}
|
||||
self.assertDictEqual(auth.data, expected_data)
|
||||
@@ -62,7 +61,6 @@ class TestAuth(unittest.TestCase):
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"uid": 1234,
|
||||
"notification_key": 1234,
|
||||
"device_id": const.DEVICE_ID,
|
||||
}
|
||||
self.assertDictEqual(auth.data, expected_data)
|
||||
@@ -165,10 +163,8 @@ class TestAuth(unittest.TestCase):
|
||||
def test_refresh_token(self, mock_login):
|
||||
"""Test refresh token method."""
|
||||
mock_login.return_value = {
|
||||
"region": {"tier": "test"},
|
||||
"authtoken": {"authtoken": "foobar"},
|
||||
"client": {"id": 1234},
|
||||
"account": {"id": 5678},
|
||||
"account": {"account_id": 5678, "client_id": 1234, "tier": "test"},
|
||||
"auth": {"token": "foobar"},
|
||||
}
|
||||
self.assertTrue(self.auth.refresh_token())
|
||||
self.assertEqual(self.auth.region_id, "test")
|
||||
@@ -190,12 +186,19 @@ class TestAuth(unittest.TestCase):
|
||||
self.auth.login_response = {}
|
||||
self.assertFalse(self.auth.check_key_required())
|
||||
|
||||
self.auth.login_response = {"client": {"verification_required": False}}
|
||||
self.auth.login_response = {"account": {"client_verification_required": False}}
|
||||
self.assertFalse(self.auth.check_key_required())
|
||||
|
||||
self.auth.login_response = {"client": {"verification_required": True}}
|
||||
self.auth.login_response = {"account": {"client_verification_required": True}}
|
||||
self.assertTrue(self.auth.check_key_required())
|
||||
|
||||
@mock.patch("blinkpy.auth.api.request_logout")
|
||||
def test_logout(self, mock_req):
|
||||
"""Test logout method."""
|
||||
mock_blink = MockBlink(None)
|
||||
mock_req.return_value = True
|
||||
self.assertTrue(self.auth.logout(mock_blink))
|
||||
|
||||
@mock.patch("blinkpy.auth.api.request_verify")
|
||||
def test_send_auth_key(self, mock_req):
|
||||
"""Check sending of auth key."""
|
||||
|
||||
+17
-1
@@ -3,7 +3,7 @@
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import time
|
||||
from blinkpy.helpers.util import json_load, Throttle, time_to_seconds
|
||||
from blinkpy.helpers.util import json_load, Throttle, time_to_seconds, gen_uid
|
||||
|
||||
|
||||
class TestUtil(unittest.TestCase):
|
||||
@@ -132,3 +132,19 @@ class TestUtil(unittest.TestCase):
|
||||
self.assertEqual(json_load("fake.file"), None)
|
||||
with mock.patch("builtins.open", mock.mock_open(read_data="")):
|
||||
self.assertEqual(json_load("fake.file"), None)
|
||||
|
||||
def test_gen_uid(self):
|
||||
"""Test gen_uid formatting."""
|
||||
val1 = gen_uid(8)
|
||||
val2 = gen_uid(8, uid_format=True)
|
||||
|
||||
self.assertEqual(len(val1), 16)
|
||||
|
||||
self.assertTrue(val2.startswith("BlinkCamera_"))
|
||||
val2_cut = val2.split("_")
|
||||
val2_split = val2_cut[1].split("-")
|
||||
self.assertEqual(len(val2_split[0]), 8)
|
||||
self.assertEqual(len(val2_split[1]), 4)
|
||||
self.assertEqual(len(val2_split[2]), 4)
|
||||
self.assertEqual(len(val2_split[3]), 4)
|
||||
self.assertEqual(len(val2_split[4]), 12)
|
||||
|
||||
Reference in New Issue
Block a user