2
2
import asyncio
3
3
import logging
4
4
from datetime import datetime , timedelta
5
+ from typing import Optional
5
6
6
7
from aiohttp import ClientSession
7
8
from aiohttp .client_exceptions import ClientError
@@ -57,11 +58,16 @@ def __init__(self, brand: str, websession: ClientSession) -> None:
57
58
raise UnsupportedBrandError ('Unknown brand: {0}' .format (brand ))
58
59
59
60
self ._brand = brand
61
+ self ._websession = websession
62
+
63
+ self ._credentials = None
60
64
self ._security_token = None
61
65
self ._devices = []
62
66
self ._last_update = None
63
- self ._websession = websession
67
+ self .online = False
68
+
64
69
self ._update_lock = asyncio .Lock ()
70
+ self ._security_token_lock = asyncio .Lock ()
65
71
66
72
async def _request (
67
73
self ,
@@ -72,8 +78,16 @@ async def _request(
72
78
params : dict = None ,
73
79
data : dict = None ,
74
80
json : dict = None ,
75
- ** kwargs ) -> dict :
76
- """Make a request."""
81
+ login_request : bool = False ,
82
+ ** kwargs ) -> Optional [dict ]:
83
+
84
+ # Get a security token if we do not have one AND this request
85
+ # is not to get a security token.
86
+ if self ._security_token is None and not login_request :
87
+ await self ._get_security_token ()
88
+ if self ._security_token is None :
89
+ return None
90
+
77
91
url = '{0}/{1}' .format (API_BASE , endpoint )
78
92
79
93
if not headers :
@@ -98,8 +112,10 @@ async def _request(
98
112
resp .raise_for_status ()
99
113
return await resp .json (content_type = None )
100
114
except asyncio .TimeoutError :
101
- timeout = timeout * 2
102
- _LOGGER .warning ('%s Timeout requesting from %s' ,
115
+ # Start increasing timeout if already tried twice..
116
+ if attempt > 1 :
117
+ timeout = timeout * 2
118
+ _LOGGER .debug ('%s Timeout requesting from %s' ,
103
119
start_request_time , endpoint )
104
120
except ClientError as err :
105
121
if attempt == DEFAULT_REQUEST_RETRIES - 1 :
@@ -119,7 +135,7 @@ async def _update_device_state(self) -> None:
119
135
async with self ._update_lock :
120
136
if datetime .utcnow () - self ._last_update > \
121
137
MIN_TIME_BETWEEN_UPDATES :
122
- await self ._get_device_states ()
138
+ self . online = await self ._get_device_states ()
123
139
124
140
async def _get_device_states (self ) -> bool :
125
141
_LOGGER .debug ('Retrieving new device states' )
@@ -129,10 +145,21 @@ async def _get_device_states(self) -> bool:
129
145
_LOGGER .error ('Getting device states failed: %s' , err )
130
146
return False
131
147
132
- if int (devices_resp .get ('ReturnCode' , 1 )) != 0 :
133
- _LOGGER .error (
134
- 'Error while retrieving states: %s' ,
135
- devices_resp .get ('ErrorMessage' , 'Unknown Error' ))
148
+ if devices_resp is None :
149
+ return False
150
+
151
+ return_code = int (devices_resp .get ('ReturnCode' , 1 ))
152
+
153
+ if return_code != 0 :
154
+ if return_code == - 3333 :
155
+ # Login error, need to retrieve a new token next time.
156
+ self ._security_token = None
157
+ _LOGGER .debug ('Security token expired' )
158
+ else :
159
+ _LOGGER .error (
160
+ 'Error %s while retrieving states: %s' ,
161
+ devices_resp .get ('ReturnCode' ),
162
+ devices_resp .get ('ErrorMessage' , 'Unknown Error' ))
136
163
return False
137
164
138
165
self ._store_device_states (devices_resp .get ('Devices' , []))
@@ -153,20 +180,39 @@ def _store_device_states(self, devices: dict) -> None:
153
180
154
181
async def authenticate (self , username : str , password : str ) -> None :
155
182
"""Authenticate against the API."""
156
- _LOGGER .debug ('Starting authentication' )
157
- login_resp = await self ._request (
158
- 'post' ,
159
- LOGIN_ENDPOINT ,
160
- json = {
161
- 'username' : username ,
162
- 'password' : password
163
- })
164
-
165
- if int (login_resp ['ReturnCode' ]) != 0 :
166
- raise MyQError (login_resp ['ErrorMessage' ])
167
-
168
- self ._security_token = login_resp ['SecurityToken' ]
169
- _LOGGER .debug ('Authentication completed' )
183
+ self ._credentials = {
184
+ 'username' : username ,
185
+ 'password' : password ,
186
+ }
187
+
188
+ await self ._get_security_token ()
189
+
190
+ async def _get_security_token (self ) -> None :
191
+ """Request a security token."""
192
+ _LOGGER .debug ('Requesting security token.' )
193
+ if self ._credentials is None :
194
+ return
195
+
196
+ # Make sure only 1 request can be sent at a time.
197
+ async with self ._security_token_lock :
198
+ # Confirm there is still no security token.
199
+ if self ._security_token is None :
200
+ login_resp = await self ._request (
201
+ 'post' ,
202
+ LOGIN_ENDPOINT ,
203
+ json = self ._credentials ,
204
+ login_request = True ,
205
+ )
206
+
207
+ return_code = int (login_resp .get ('ReturnCode' , 1 ))
208
+ if return_code != 0 :
209
+ if return_code == 203 :
210
+ # Invalid username or password.
211
+ _LOGGER .debug ('Invalid username or password' )
212
+ self ._credentials = None
213
+ raise MyQError (login_resp ['ErrorMessage' ])
214
+
215
+ self ._security_token = login_resp ['SecurityToken' ]
170
216
171
217
async def get_devices (self , covers_only : bool = True ) -> list :
172
218
"""Get a list of all devices associated with the account."""
@@ -177,6 +223,9 @@ async def get_devices(self, covers_only: bool = True) -> list:
177
223
# print(json.dumps(devices_resp, indent=4))
178
224
179
225
device_list = []
226
+ if devices_resp is None :
227
+ return device_list
228
+
180
229
for device in devices_resp ['Devices' ]:
181
230
if not covers_only or \
182
231
device ['MyQDeviceTypeName' ] in SUPPORTED_DEVICE_TYPE_NAMES :
0 commit comments