Skip to content

Commit 2d649f6

Browse files
authored
Merge branch 'fronzbot:dev' into dev
2 parents a002e90 + e2e1d65 commit 2d649f6

File tree

12 files changed

+231
-223
lines changed

12 files changed

+231
-223
lines changed

blinkapp/blinkapp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from blinkpy.helpers.util import json_load
1010

1111
CREDFILE = environ.get("CREDFILE")
12-
TIMEDELTA = timedelta(environ.get("TIMEDELTA", "1"))
12+
TIMEDELTA = timedelta(int(environ.get("TIMEDELTA", "1")))
1313

1414

1515
def get_date():

blinkpy/api.py

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@
99
Throttle,
1010
local_storage_clip_url_template,
1111
)
12-
from blinkpy.helpers.constants import DEFAULT_URL, TIMEOUT, DEFAULT_USER_AGENT
12+
from blinkpy.helpers.constants import (
13+
TIMEOUT,
14+
DEFAULT_USER_AGENT,
15+
OAUTH_CLIENT_ID,
16+
OAUTH_GRANT_TYPE_PASSWORD,
17+
OAUTH_GRANT_TYPE_REFRESH_TOKEN,
18+
OAUTH_SCOPE,
19+
)
20+
from urllib.parse import urlencode
1321

1422
_LOGGER = logging.getLogger(__name__)
1523

@@ -22,32 +30,44 @@ async def request_login(
2230
auth,
2331
url,
2432
login_data,
33+
is_refresh=False,
2534
is_retry=False,
2635
):
2736
"""
28-
Login request.
37+
OAuth login request.
2938
3039
:param auth: Auth instance.
3140
:param url: Login url.
3241
:param login_data: Dictionary containing blink login data.
3342
:param is_retry:
43+
:param two_fa_code: 2FA code if required
3444
"""
45+
3546
headers = {
36-
"Host": DEFAULT_URL,
37-
"Content-Type": "application/json",
38-
"user-agent": DEFAULT_USER_AGENT,
47+
"Content-Type": "application/x-www-form-urlencoded",
48+
"User-Agent": DEFAULT_USER_AGENT,
49+
"hardware_id": login_data.get("device_id", "Blinkpy"),
3950
}
4051

41-
data = dumps(
42-
{
43-
"email": login_data["username"],
44-
"password": login_data["password"],
45-
"unique_id": login_data["uid"],
46-
"device_identifier": login_data["device_id"],
47-
"client_name": "Computer",
48-
"reauth": True,
49-
}
50-
)
52+
# Add 2FA code to headers if provided
53+
if "2fa_code" in login_data:
54+
headers["2fa-code"] = login_data["2fa_code"]
55+
56+
# Prepare form data for OAuth
57+
form_data = {
58+
"username": login_data["username"],
59+
"client_id": OAUTH_CLIENT_ID,
60+
"scope": OAUTH_SCOPE,
61+
}
62+
63+
if is_refresh:
64+
form_data["grant_type"] = OAUTH_GRANT_TYPE_REFRESH_TOKEN
65+
form_data["refresh_token"] = auth._refresh_token
66+
else:
67+
form_data["grant_type"] = OAUTH_GRANT_TYPE_PASSWORD
68+
form_data["password"] = login_data["password"]
69+
70+
data = urlencode(form_data)
5171

5272
return await auth.query(
5373
url=url,
@@ -56,23 +76,23 @@ async def request_login(
5676
json_resp=False,
5777
reqtype="post",
5878
is_retry=is_retry,
79+
skip_refresh_check=True,
5980
)
6081

6182

62-
async def request_verify(auth, blink, verify_key):
63-
"""Send verification key to blink servers."""
64-
url = (
65-
f"{blink.urls.base_url}/api/v5/accounts/{blink.account_id}"
66-
f"/users/{blink.auth.user_id}"
67-
f"/clients/{blink.client_id}/client_verification/pin/verify"
68-
)
69-
data = dumps({"pin": verify_key})
83+
async def request_tier(auth, url):
84+
"""Get account tier information from blink servers."""
85+
headers = {
86+
"Content-Type": "application/x-www-form-urlencoded",
87+
"User-Agent": DEFAULT_USER_AGENT,
88+
"Authorization": f"Bearer {auth.token}",
89+
}
90+
7091
return await auth.query(
7192
url=url,
72-
headers=auth.header,
73-
data=data,
74-
json_resp=False,
75-
reqtype="post",
93+
headers=headers,
94+
json_resp=True,
95+
reqtype="get",
7696
)
7797

7898

@@ -214,7 +234,7 @@ async def request_command_status(blink, network, command_id):
214234
async def request_homescreen(blink, **kwargs):
215235
"""Request homescreen info."""
216236
url = f"{blink.urls.base_url}/api/v3/accounts/{blink.account_id}/homescreen"
217-
return await http_get(blink, url)
237+
return await http_get(blink, url, json=False)
218238

219239

220240
@Throttle(seconds=MIN_THROTTLE_TIME)

blinkpy/auth.py

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Login handler for blink."""
22

3+
import time
34
import logging
45
from aiohttp import (
56
ClientSession,
@@ -14,6 +15,7 @@
1415
APP_BUILD,
1516
DEFAULT_USER_AGENT,
1617
LOGIN_ENDPOINT,
18+
TIER_ENDPOINT,
1719
TIMEOUT,
1820
)
1921

@@ -45,12 +47,16 @@ def __init__(
4547
login_data = {}
4648
self.data = login_data
4749
self.token = login_data.get("token", None)
50+
self.expires_in = login_data.get("expires_in", None)
51+
self.expiration_date = None
52+
self._refresh_token = login_data.get("refresh_token", None)
4853
self.host = login_data.get("host", None)
4954
self.region_id = login_data.get("region_id", None)
5055
self.client_id = login_data.get("client_id", None)
5156
self.account_id = login_data.get("account_id", None)
5257
self.user_id = login_data.get("user_id", None)
5358
self.login_response = None
59+
self.tier_info = None
5460
self.is_errored = False
5561
self.no_prompt = no_prompt
5662
self._agent = agent
@@ -61,6 +67,8 @@ def __init__(
6167
def login_attributes(self):
6268
"""Return a dictionary of login attributes."""
6369
self.data["token"] = self.token
70+
self.data["expires_in"] = self.expires_in
71+
self.data["refresh_token"] = self._refresh_token
6472
self.data["host"] = self.host
6573
self.data["region_id"] = self.region_id
6674
self.data["client_id"] = self.client_id
@@ -74,9 +82,9 @@ def header(self):
7482
if self.token is None:
7583
return None
7684
return {
77-
"APP-BUILD": self._app_build,
78-
"TOKEN_AUTH": self.token,
79-
"User-Agent": self._agent,
85+
# "APP-BUILD": self._app_build,
86+
"Authorization": f"Bearer {self.token}",
87+
# "User-Agent": self._agent,
8088
"Content-Type": "application/json",
8189
}
8290

@@ -88,23 +96,36 @@ def validate_login(self):
8896
self.data = util.prompt_login_data(self.data)
8997
self.data = util.validate_login_data(self.data)
9098

91-
async def login(self, login_url=LOGIN_ENDPOINT):
92-
"""Attempt login to blink servers."""
99+
def validate_2fa(self):
100+
"""Check 2FA information and prompt if not available."""
101+
self.data["2fa_code"] = self.data.get("2fa_code", None)
102+
if not self.no_prompt:
103+
self.data = util.prompt_2fa_data(self.data)
104+
105+
async def login(self, login_url=LOGIN_ENDPOINT, refresh=False):
106+
"""Attempt OAuth login to blink servers."""
93107
self.validate_login()
94-
_LOGGER.info("Attempting login with %s", login_url)
95108
response = await api.request_login(
96109
self,
97110
login_url,
98111
self.data,
112+
is_refresh=refresh,
99113
is_retry=False,
100114
)
101115
try:
102116
if response.status == 200:
103117
return await response.json()
118+
if response.status == 412:
119+
self.validate_2fa()
120+
return await self.login()
104121
raise LoginError
105122
except AttributeError as error:
106123
raise LoginError from error
107124

125+
async def get_tier_info(self, tier_url=TIER_ENDPOINT):
126+
"""Get tier information."""
127+
return await api.request_tier(self, tier_url)
128+
108129
def logout(self, blink):
109130
"""Log out."""
110131
return api.request_logout(blink)
@@ -116,6 +137,8 @@ async def refresh_token(self):
116137
_LOGGER.info("Token expired, attempting automatic refresh.")
117138
self.login_response = await self.login()
118139
self.extract_login_info()
140+
self.tier_info = await self.get_tier_info()
141+
self.extract_tier_info()
119142
self.is_errored = False
120143
except LoginError as error:
121144
_LOGGER.error("Login endpoint failed. Try again later.")
@@ -127,12 +150,16 @@ async def refresh_token(self):
127150

128151
def extract_login_info(self):
129152
"""Extract login info from login response."""
130-
self.region_id = self.login_response["account"]["tier"]
153+
self.token = self.login_response["access_token"]
154+
self.expires_in = self.login_response["expires_in"]
155+
self.expiration_date = time.time() + self.expires_in
156+
self._refresh_token = self.login_response["refresh_token"]
157+
158+
def extract_tier_info(self):
159+
"""Extract tier info from tier info response."""
160+
self.region_id = self.tier_info["tier"]
131161
self.host = f"{self.region_id}.{BLINK_URL}"
132-
self.token = self.login_response["auth"]["token"]
133-
self.client_id = self.login_response["account"]["client_id"]
134-
self.account_id = self.login_response["account"]["account_id"]
135-
self.user_id = self.login_response["account"].get("user_id", None)
162+
self.account_id = self.tier_info["account_id"]
136163

137164
async def startup(self):
138165
"""Initialize tokens for communication."""
@@ -161,6 +188,13 @@ async def validate_response(self, response: ClientResponse, json_resp):
161188
self.is_errored = False
162189
return json_data
163190

191+
def need_refresh(self):
192+
"""Check if token needs refresh."""
193+
if self.expiration_date is None:
194+
return self._refresh_token is not None
195+
196+
return self.expiration_date - time.time() < 60
197+
164198
async def query(
165199
self,
166200
url=None,
@@ -171,6 +205,7 @@ async def query(
171205
json_resp=True,
172206
is_retry=False,
173207
timeout=TIMEOUT,
208+
skip_refresh_check=False,
174209
):
175210
"""Perform server requests.
176211
@@ -183,6 +218,9 @@ async def query(
183218
:param is_retry: Is this part of a re-auth attempt? True/FALSE
184219
"""
185220
try:
221+
if not skip_refresh_check and self.need_refresh():
222+
await self.login(refresh=True)
223+
186224
if reqtype == "get":
187225
response = await self.session.get(
188226
url=url, data=data, headers=headers, timeout=timeout
@@ -231,33 +269,6 @@ async def query(
231269
_LOGGER.error("Unable to refresh token.")
232270
return None
233271

234-
async def send_auth_key(self, blink, key):
235-
"""Send 2FA key to blink servers."""
236-
if key is not None:
237-
response = await api.request_verify(self, blink, key)
238-
try:
239-
json_resp = await response.json()
240-
blink.available = json_resp["valid"]
241-
if not blink.available:
242-
_LOGGER.error("%s", json_resp["message"])
243-
return False
244-
except (KeyError, TypeError, ContentTypeError) as er:
245-
_LOGGER.error(
246-
"Did not receive valid response from server. Error: %s",
247-
er,
248-
)
249-
return False
250-
return True
251-
252-
def check_key_required(self):
253-
"""Check if 2FA key is required."""
254-
try:
255-
if self.login_response["account"]["client_verification_required"]:
256-
return True
257-
except (KeyError, TypeError):
258-
pass
259-
return False
260-
261272

262273
class TokenRefreshFailed(Exception):
263274
"""Class to throw failed refresh exception."""

0 commit comments

Comments
 (0)