tplinkrouterc6u 5.12.4__py3-none-any.whl → 5.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,198 @@
1
+ import base64
2
+ import re
3
+ from time import sleep
4
+ from urllib.parse import urlparse
5
+
6
+ import requests
7
+ from requests import Response
8
+
9
+ from tplinkrouterc6u.client.mr200 import TPLinkMR200Client
10
+ from tplinkrouterc6u.common.package_enum import VPN
11
+ from tplinkrouterc6u.common.dataclass import (
12
+ LTEStatus,
13
+ )
14
+ from tplinkrouterc6u.common.exception import ClientException, ClientError
15
+ from tplinkrouterc6u.common.package_enum import Connection
16
+
17
+
18
+ class TplinkC3200Router(TPLinkMR200Client):
19
+ # This is a session variable that will contain everything needed as soon as the router is connected.
20
+ # - The "Referer" header which allows to be accepted by the CGI module.
21
+ # - the Authentification cookie
22
+ SESSION: requests.Session
23
+
24
+ # Possible retries limit
25
+ REQUEST_RETRIES = 1
26
+
27
+ # Router name to be included in logs for example,
28
+ # or to be redefined by subclasses.
29
+ ROUTER_NAME = "TP Link Router C3200"
30
+
31
+ # Connection method
32
+ def supports(self) -> bool:
33
+ if len(self.password) > 125:
34
+ return False
35
+
36
+ try:
37
+ # This method checks if we can recognize the router type.
38
+ welcome_page = requests.get(self.host, timeout=5)
39
+ if welcome_page and welcome_page.status_code == 200 and re.search("Archer", welcome_page.text):
40
+ return True
41
+ except ClientException:
42
+ pass
43
+
44
+ return False
45
+
46
+ def authorize(self) -> None:
47
+
48
+ # ———————————————————————————————————————————
49
+ # Create the SESSION object and the authorization cookie
50
+ # ———————————————————————————————————————————
51
+ self.SESSION = requests.Session()
52
+
53
+ if self._logger:
54
+ self._logger.debug("!")
55
+ # We need to extract the domain form the host to fill the cookie.
56
+ router_host = urlparse(self.host).hostname
57
+ if not router_host:
58
+ raise ValueError(self.host & " must contain a valid host, ex. http://192.168.168.1")
59
+
60
+ """Return the string "Basic <base64(username:password)>"."""
61
+ token_bytes = f"{self.username}:{self.password}".encode()
62
+ encoded = base64.b64encode(token_bytes).decode()
63
+ auth_cookie_value = f"Basic {encoded}"
64
+
65
+ self.SESSION.cookies.set(
66
+ name="Authorization",
67
+ value=auth_cookie_value,
68
+ domain=router_host,
69
+ path="/",
70
+ )
71
+
72
+ self.SESSION.headers = {"Referer": f"{self.host}/", "Origin": self.host}
73
+
74
+ login_url = '{}/'.format(self.host)
75
+ response = Response()
76
+
77
+ try:
78
+ response = self.SESSION.post(login_url, timeout=10)
79
+ except Exception as e:
80
+ error = self.ROUTER_NAME + " - Cannot authorize! Error - {}; Response - {}".format(e, response.text)
81
+ if self._logger:
82
+ self._logger.debug(error)
83
+ raise ClientException(error)
84
+
85
+ def logout(self) -> None:
86
+ self.SESSION.cookies.clear(domain=urlparse(self.host).hostname, path="/")
87
+
88
+ def get_lte_status(self) -> LTEStatus:
89
+ pass
90
+
91
+ def _get_params(self, retry=False) -> None:
92
+ pass
93
+
94
+ def set_vpn(self, vpn: VPN, enable: bool) -> None:
95
+ # Unable to test it on my C3200
96
+ pass
97
+
98
+ def req_act(self, acts: list):
99
+ act_types, act_data = self._fill_acts(acts)
100
+
101
+ url = f"{self.host}/cgi?" + '&'.join(act_types)
102
+ data_str = ''.join(act_data)
103
+ (code, response) = self._request(url, data_str=data_str)
104
+
105
+ if code != 200:
106
+ error = self.ROUTER_NAME + ' - Response with error; Request {} - Response {}'.format(data_str, response)
107
+ if self._logger:
108
+ self._logger.debug(error)
109
+ raise ClientError(error)
110
+
111
+ result = self._merge_response(response)
112
+
113
+ return response, result.get('0') if len(result) == 1 and result.get('0') else result
114
+
115
+ def _request(self, url, method='POST', data_str=None, encrypt=False, is_login=False):
116
+ r = Response()
117
+
118
+ retry = 0
119
+ while retry < self.REQUEST_RETRIES:
120
+ # send the request
121
+ if method == 'POST':
122
+ r = self.SESSION.post(url, data=data_str)
123
+ elif method == 'GET':
124
+ r = self.SESSION.get(url, data=data_str)
125
+ else:
126
+ raise Exception('Unsupported method ' + str(method))
127
+
128
+ # sometimes we get 500 here, not sure why... just retry the request
129
+ if (r.status_code not in [500, 406]
130
+ and '<title>500 Internal Server Error</title>' not in r.text
131
+ and '<title>406 Not Acceptable</title>' not in r.text):
132
+ break
133
+
134
+ sleep(0.1)
135
+ retry += 1
136
+
137
+ return r.status_code, r.text
138
+
139
+ # Overriding the method since we have two 5G bands in the rooter.
140
+ # We manage both as one.
141
+ def set_wifi(self, wifi: Connection, enable: bool) -> None:
142
+ acts = []
143
+
144
+ match wifi:
145
+ case Connection.HOST_2G:
146
+ acts = [
147
+ self.ActItem(
148
+ self.ActItem.SET,
149
+ 'LAN_WLAN',
150
+ '1,1,0,0,0,0',
151
+ attrs=['enable={}'.format(int(enable))]),
152
+ ]
153
+ case Connection.HOST_5G:
154
+ # We activate both 5G bands
155
+ acts = [
156
+ self.ActItem(
157
+ self.ActItem.SET,
158
+ 'LAN_WLAN',
159
+ '1,2,0,0,0,0',
160
+ attrs=['enable={}'.format(int(enable))]),
161
+ self.ActItem(
162
+ self.ActItem.SET,
163
+ 'LAN_WLAN',
164
+ '1,3,0,0,0,0',
165
+ attrs=['enable={}'.format(int(enable))]),
166
+ ]
167
+ case Connection.GUEST_2G:
168
+ acts = [
169
+ self.ActItem(
170
+ self.ActItem.SET,
171
+ 'LAN_WLAN_MSSIDENTRY',
172
+ '1,1,1,0,0,0',
173
+ attrs=['enable={}'.format(int(enable))]),
174
+ ]
175
+ case Connection.GUEST_5G:
176
+ acts = [
177
+ self.ActItem(
178
+ self.ActItem.SET,
179
+ 'LAN_WLAN_MSSIDENTRY',
180
+ '1,2,1,0,0,0',
181
+ attrs=['enable={}'.format(int(enable))]),
182
+ self.ActItem(
183
+ self.ActItem.SET,
184
+ 'LAN_WLAN_MSSIDENTRY',
185
+ '1,3,1,0,0,0',
186
+ attrs=['enable={}'.format(int(enable))]),
187
+ ]
188
+ self.req_act(acts)
189
+
190
+ def reboot(self) -> None:
191
+ # CGI 7 et [ACT_REBOOT#0,0,0,0,0,0#0,0,0,0,0,0]0,0
192
+
193
+ acts = [
194
+ self.ActItem(self.ActItem.OP, 'ACT_REBOOT'),
195
+ ]
196
+ _, values = self.req_act(acts)
197
+
198
+ # print(values.keys())
@@ -490,3 +490,100 @@ class TplinkRouter(TplinkEncryption, TplinkBaseRouter):
490
490
  self._url_pptpd = 'admin/pptpd?form=config'
491
491
  self._url_vpnconn_openvpn = 'admin/vpnconn?form=config'
492
492
  self._url_vpnconn_pptpd = 'admin/vpnconn?form=config'
493
+
494
+
495
+ class TplinkRouterV1_11(TplinkBaseRouter):
496
+ """
497
+ Router client for newer TP-Link firmware (1.11.0+) that uses simplified
498
+ RSA-only authentication without AES encryption wrapper.
499
+
500
+ Based on fix from: https://github.com/AlexandrErohin/TP-Link-Archer-C6U/issues/90
501
+ """
502
+
503
+ def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
504
+ verify_ssl: bool = True, timeout: int = 30) -> None:
505
+ super().__init__(host, password, username, logger, verify_ssl, timeout)
506
+ self._pwdNN = ''
507
+ self._pwdEE = ''
508
+
509
+ def supports(self) -> bool:
510
+ """Check if this client can handle the router (new firmware with RSA-only auth)."""
511
+ if len(self.password) > 125:
512
+ return False
513
+
514
+ try:
515
+ self._request_pwd()
516
+ # V1_11 uses 2048-bit RSA = 512 hex chars, older firmware uses 1024-bit = 256 chars
517
+ return len(self._pwdNN) >= 512
518
+ except Exception:
519
+ return False
520
+
521
+ def _request_pwd(self) -> None:
522
+ """Get RSA public key for password encryption."""
523
+ url = '{}/cgi-bin/luci/;stok=/login?form=keys'.format(self.host)
524
+
525
+ response = post(
526
+ url,
527
+ params={'operation': 'read'},
528
+ timeout=self.timeout,
529
+ verify=self._verify_ssl,
530
+ )
531
+
532
+ try:
533
+ data = response.json()
534
+ self._pwdNN = data[self._data_block]['password'][0]
535
+ self._pwdEE = data[self._data_block]['password'][1]
536
+ except Exception as e:
537
+ error = ('TplinkRouter - {} - Failed to get encryption keys! Error - {}; Response - {}'
538
+ .format(self.__class__.__name__, e, response.text))
539
+ if self._logger:
540
+ self._logger.debug(error)
541
+ raise ClientException(error)
542
+
543
+ def authorize(self) -> None:
544
+ """Authorize using simplified RSA-only authentication (no AES encryption)."""
545
+ if self._pwdNN == '':
546
+ self._request_pwd()
547
+
548
+ # RSA encrypt password using existing utility
549
+ encrypted_pwd = EncryptionWrapper.rsa_encrypt(self.password, self._pwdNN, self._pwdEE)
550
+
551
+ # Simple login - just operation=login&password=<HEX>
552
+ url = '{}/cgi-bin/luci/;stok=/login?form=login'.format(self.host)
553
+ response = post(
554
+ url,
555
+ data='operation=login&password={}'.format(encrypted_pwd),
556
+ headers=self._headers_login,
557
+ timeout=self.timeout,
558
+ verify=self._verify_ssl,
559
+ )
560
+
561
+ try:
562
+ data = response.json()
563
+ if not data.get('success'):
564
+ error_info = data.get(self._data_block, {})
565
+ raise ClientException(
566
+ 'TplinkRouter - {} - Login failed: {}'.format(
567
+ self.__class__.__name__,
568
+ error_info.get('errorcode', 'unknown error')
569
+ )
570
+ )
571
+
572
+ self._stok = data[self._data_block]['stok']
573
+
574
+ # Get sysauth cookie
575
+ if 'set-cookie' in response.headers:
576
+ regex_result = search(r'sysauth=([^;]+)', response.headers['set-cookie'])
577
+ if regex_result:
578
+ self._sysauth = regex_result.group(1)
579
+
580
+ self._logged = True
581
+
582
+ except ClientException:
583
+ raise
584
+ except Exception as e:
585
+ error = ('TplinkRouter - {} - Cannot authorize! Error - {}; Response - {}'
586
+ .format(self.__class__.__name__, e, response.text))
587
+ if self._logger:
588
+ self._logger.debug(error)
589
+ raise ClientException(error)
@@ -92,7 +92,7 @@ class TPLinkEXClient(TPLinkMRClientBase):
92
92
  self.ActItem(self.ActItem.GL, 'DEV2_ADT_LAN', attrs=['MACAddress', 'IPAddress']),
93
93
  self.ActItem(self.ActItem.GL, 'DEV2_ADT_WAN',
94
94
  attrs=['enable', 'MACAddr', 'connIPv4Address', 'connIPv4Gateway']),
95
- self.ActItem(self.ActItem.GL, 'DEV2_ADT_WIFI_COMMON', attrs=['primaryEnable', 'guestEnable']),
95
+ self.ActItem(self.ActItem.GL, 'DEV2_ADT_WIFI_COMMON'),
96
96
  self.ActItem(self.ActItem.GL, 'DEV2_HOST_ENTRY',
97
97
  attrs=['active', 'X_TP_LanConnType', 'physAddress', 'IPAddress', 'hostName']),
98
98
  self.ActItem(self.ActItem.GO, 'DEV2_MEM_STATUS', attrs=['total', 'free']),
@@ -114,17 +114,17 @@ class TPLinkEXClient(TPLinkMRClientBase):
114
114
  status._wan_ipv4_addr = IPv4Address(item['connIPv4Address']) if item.get('connIPv4Address') else None
115
115
  status._wan_ipv4_gateway = IPv4Address(item['connIPv4Gateway']) if item.get('connIPv4Address') else None
116
116
 
117
- if values[2].__class__ != list:
118
- status.wifi_2g_enable = bool(int(values[2]['primaryEnable']))
119
- else:
120
- status.wifi_2g_enable = bool(int(values[2][0]['primaryEnable']))
121
- status.wifi_5g_enable = bool(int(values[2][1]['primaryEnable']))
122
-
123
- if values[2].__class__ != list:
124
- status.guest_2g_enable = bool(int(values[2]['guestEnable']))
125
- else:
126
- status.guest_2g_enable = bool(int(values[2][0]['guestEnable']))
127
- status.guest_5g_enable = bool(int(values[2][1]['guestEnable']))
117
+ if values[2]:
118
+ if values[2].__class__ != list:
119
+ status.wifi_2g_enable = bool(int(values[2]['primaryEnable']))
120
+ status.guest_2g_enable = bool(int(values[2]['guestEnable'])) if values[2].get('guestEnable') else None
121
+ else:
122
+ status.wifi_2g_enable = bool(int(values[2][0]['primaryEnable']))
123
+ status.wifi_5g_enable = bool(int(values[2][1]['primaryEnable']))
124
+ status.guest_2g_enable = bool(int(values[2][0]['guestEnable'])) \
125
+ if values[2][0].get('guestEnable') else None
126
+ status.guest_5g_enable = bool(int(values[2][1]['guestEnable'])) \
127
+ if values[2][1].get('guestEnable') else None
128
128
 
129
129
  devices = {}
130
130
  for val in self._to_list(values[3]):
@@ -2,7 +2,7 @@ from hashlib import md5
2
2
  from re import search
3
3
  from time import time, sleep
4
4
  from urllib.parse import quote
5
- from requests import Session
5
+ from requests import Session, Response
6
6
  from datetime import timedelta, datetime
7
7
  from macaddress import EUI48
8
8
  from ipaddress import IPv4Address
@@ -23,6 +23,7 @@ from tplinkrouterc6u.common.dataclass import (
23
23
  )
24
24
  from tplinkrouterc6u.common.exception import ClientException, ClientError
25
25
  from tplinkrouterc6u.client_abstract import AbstractRouter
26
+ from typing import List
26
27
 
27
28
 
28
29
  class TPLinkMRClientBase(AbstractRouter):
@@ -54,6 +55,10 @@ class TPLinkMRClientBase(AbstractRouter):
54
55
  Connection.GUEST_5G: '1,2,1,0,0,0',
55
56
  }
56
57
 
58
+ # Router name to be included in logs for example,
59
+ # or to be redefined by subclasses.
60
+ ROUTER_NAME = "TP Link Router MR"
61
+
57
62
  class ActItem:
58
63
  GET = 1
59
64
  SET = 2
@@ -65,7 +70,9 @@ class TPLinkMRClientBase(AbstractRouter):
65
70
  CGI = 8
66
71
 
67
72
  def __init__(self, type: int, oid: str, stack: str = '0,0,0,0,0,0', pstack: str = '0,0,0,0,0,0',
68
- attrs: list = []):
73
+ attrs=None):
74
+ if attrs is None:
75
+ attrs = []
69
76
  self.type = type
70
77
  self.oid = oid
71
78
  self.stack = stack
@@ -111,6 +118,10 @@ class TPLinkMRClientBase(AbstractRouter):
111
118
  self._token = self._req_token()
112
119
  self._authorized_at = datetime.now()
113
120
 
121
+ # Fake implementation of all abstract methods
122
+ def logout(self) -> None:
123
+ pass
124
+
114
125
  def reboot(self) -> None:
115
126
  acts = [
116
127
  self.ActItem(self.ActItem.OP, 'ACT_REBOOT')
@@ -215,9 +226,9 @@ class TPLinkMRClientBase(AbstractRouter):
215
226
 
216
227
  return status
217
228
 
218
- def get_ipv4_reservations(self) -> [IPv4Reservation]:
229
+ def get_ipv4_reservations(self) -> List[IPv4Reservation]:
219
230
  acts = [
220
- self.ActItem(self.ActItem.GL, 'LAN_DHCP_STATIC_ADDR', attrs=['enable', 'chaddr', 'yiaddr']),
231
+ self.ActItem(self.ActItem.GL, 'LAN_DHCP_STATIC_ADDR'),
221
232
  ]
222
233
  _, values = self.req_act(acts)
223
234
 
@@ -227,13 +238,13 @@ class TPLinkMRClientBase(AbstractRouter):
227
238
  IPv4Reservation(
228
239
  EUI48(item['chaddr']),
229
240
  IPv4Address(item['yiaddr']),
230
- '',
241
+ item.get('description', ''),
231
242
  bool(int(item['enable']))
232
243
  ))
233
244
 
234
245
  return ipv4_reservations
235
246
 
236
- def get_ipv4_dhcp_leases(self) -> [IPv4DHCPLease]:
247
+ def get_ipv4_dhcp_leases(self) -> List[IPv4DHCPLease]:
237
248
  acts = [
238
249
  self.ActItem(self.ActItem.GL, 'LAN_HOST_ENTRY', attrs=['IPAddress', 'MACAddress', 'hostName',
239
250
  'leaseTimeRemaining']),
@@ -324,12 +335,11 @@ class TPLinkMRClientBase(AbstractRouter):
324
335
 
325
336
  self.req_act(acts)
326
337
 
327
- def req_act(self, acts: list):
328
- '''
329
- Requests ACTs via the cgi_gdpr proxy
330
- '''
331
- act_types = []
332
- act_data = []
338
+ @staticmethod
339
+ def _fill_acts(acts: list) -> tuple[List[str], List[str]]:
340
+ # Fill the act lists, to refactor for other subclasses.
341
+ act_types: List[str] = []
342
+ act_data: List[str] = []
333
343
 
334
344
  for act in acts:
335
345
  act_types.append(str(act.type))
@@ -342,13 +352,18 @@ class TPLinkMRClientBase(AbstractRouter):
342
352
  '\r\n'.join(act.attrs)
343
353
  ))
344
354
 
355
+ return act_types, act_data
356
+
357
+ def req_act(self, acts: list):
358
+ act_types, act_data = self._fill_acts(acts)
359
+
345
360
  data = '&'.join(act_types) + '\r\n' + ''.join(act_data)
346
361
 
347
362
  url = self._get_url('cgi_gdpr')
348
363
  (code, response) = self._request(url, data_str=data, encrypt=True)
349
364
 
350
365
  if code != 200:
351
- error = 'TplinkRouter - MR - Response with error; Request {} - Response {}'.format(data, response)
366
+ error = self.ROUTER_NAME + ' - Response with error; Request {} - Response {}'.format(data, response)
352
367
  if self._logger:
353
368
  self._logger.debug(error)
354
369
  raise ClientError(error)
@@ -391,7 +406,11 @@ class TPLinkMRClientBase(AbstractRouter):
391
406
 
392
407
  return result if result else []
393
408
 
394
- def _get_url(self, endpoint: str, params: dict = {}, include_ts: bool = True) -> str:
409
+ def _get_url(self, endpoint: str, params: dict = None, include_ts: bool = True) -> str:
410
+ # check if there is any param
411
+ if params is None:
412
+ params = {}
413
+
395
414
  # add timestamp param
396
415
  if include_ts:
397
416
  params['_'] = str(round(time() * 1000))
@@ -410,14 +429,14 @@ class TPLinkMRClientBase(AbstractRouter):
410
429
  )
411
430
 
412
431
  def _req_token(self):
413
- '''
432
+ """
414
433
  Requests the TokenID, used for CGI authentication (together with cookies)
415
434
  - token is inlined as JS var in the index (/) html page
416
435
  e.g.: <script type="text/javascript">var token="086724f57013f16e042e012becf825";</script>
417
436
 
418
437
  Return value:
419
438
  TokenID string
420
- '''
439
+ """
421
440
  url = self._get_url('')
422
441
  (code, response) = self._request(url, method='GET')
423
442
  assert code == 200
@@ -430,12 +449,12 @@ class TPLinkMRClientBase(AbstractRouter):
430
449
  return result.group(1)
431
450
 
432
451
  def _req_rsa_key(self):
433
- '''
452
+ """
434
453
  Requests the RSA public key from the host
435
454
 
436
455
  Return value:
437
456
  ((n, e), seq) tuple
438
- '''
457
+ """
439
458
  response = ''
440
459
  try:
441
460
  url = self._get_url(self._url_rsa_key)
@@ -459,7 +478,7 @@ class TPLinkMRClientBase(AbstractRouter):
459
478
  assert seq.isnumeric()
460
479
 
461
480
  except Exception as e:
462
- error = ('TplinkRouter - {} - Unknown error rsa_key! Error - {}; Response - {}'
481
+ error = (self.ROUTER_NAME + '- {} - Unknown error rsa_key! Error - {}; Response - {}'
463
482
  .format(self.__class__.__name__, e, response))
464
483
  if self._logger:
465
484
  self._logger.debug(error)
@@ -468,7 +487,7 @@ class TPLinkMRClientBase(AbstractRouter):
468
487
  return nn, ee, int(seq)
469
488
 
470
489
  def _req_login(self) -> None:
471
- '''
490
+ """
472
491
  Authenticates to the host
473
492
  - sets the session token after successful login
474
493
  - data/signature is passed as a GET parameter, NOT as a raw request data
@@ -476,7 +495,7 @@ class TPLinkMRClientBase(AbstractRouter):
476
495
 
477
496
  Example session token (set as a cookie):
478
497
  {'JSESSIONID': '4d786fede0164d7613411c7b6ec61e'}
479
- '''
498
+ """
480
499
  # encrypt username + password
481
500
 
482
501
  sign, data = self._prepare_data(self.username + '\n' + self.password, True)
@@ -501,12 +520,12 @@ class TPLinkMRClientBase(AbstractRouter):
501
520
  info = search('var currAuthTimes=(.*);\nvar currForbidTime=(.*);', response)
502
521
  assert info is not None
503
522
 
504
- error = 'TplinkRouter - MR - Login failed, wrong password. Auth times: {}/5, Forbid time: {}'.format(
523
+ error = self.ROUTER_NAME + ' - Login failed, wrong password. Auth times: {}/5, Forbid time: {}'.format(
505
524
  info.group(1), info.group(2))
506
525
  elif ret_code == self.HTTP_ERR_USER_BAD_REQUEST:
507
- error = 'TplinkRouter - MR - Login failed. Generic error code: {}'.format(ret_code)
526
+ error = self.ROUTER_NAME + ' - Login failed. Generic error code: {}'.format(ret_code)
508
527
  elif ret_code != self.HTTP_RET_OK:
509
- error = 'TplinkRouter - MR - Login failed. Unknown error code: {}'.format(ret_code)
528
+ error = self.ROUTER_NAME + ' - Login failed. Unknown error code: {}'.format(ret_code)
510
529
 
511
530
  if error:
512
531
  if self._logger:
@@ -514,14 +533,14 @@ class TPLinkMRClientBase(AbstractRouter):
514
533
  raise ClientException(error)
515
534
 
516
535
  def _request(self, url, method='POST', data_str=None, encrypt=False, is_login=False):
517
- '''
536
+ """
518
537
  Prepares and sends an HTTP request to the host
519
538
  - sets up the headers, handles token auth
520
539
  - encrypts/decrypts the data, if needed
521
540
 
522
541
  Return value:
523
542
  (status_code, response_text) tuple
524
- '''
543
+ """
525
544
  headers = self.HEADERS
526
545
 
527
546
  # add referer to request headers,
@@ -541,6 +560,8 @@ class TPLinkMRClientBase(AbstractRouter):
541
560
  data = data_str
542
561
 
543
562
  retry = 0
563
+ r = Response()
564
+
544
565
  while retry < self.REQUEST_RETRIES:
545
566
  # send the request
546
567
  if method == 'POST':
@@ -566,12 +587,12 @@ class TPLinkMRClientBase(AbstractRouter):
566
587
  return r.status_code, r.text
567
588
 
568
589
  def _parse_ret_val(self, response_text):
569
- '''
590
+ """
570
591
  Parses $.ret value from the response text
571
592
 
572
593
  Return value:
573
594
  return code (int)
574
- '''
595
+ """
575
596
  result = search(r'\$\.ret=(.*);', response_text)
576
597
  assert result is not None
577
598
  assert result.group(1).isnumeric()
@@ -618,6 +639,8 @@ class TPLinkMRClientBaseGCM(TPLinkMRClientBase):
618
639
  data = data_str
619
640
 
620
641
  retry = 0
642
+ r = Response()
643
+
621
644
  while retry < self.REQUEST_RETRIES:
622
645
  # send the request
623
646
  if method == 'POST':
@@ -656,9 +679,9 @@ class TPLinkMRClientBaseGCM(TPLinkMRClientBase):
656
679
  class TPLinkMRClient(TPLinkMRClientBase):
657
680
 
658
681
  def logout(self) -> None:
659
- '''
682
+ """
660
683
  Logs out from the host
661
- '''
684
+ """
662
685
  if self._token is None:
663
686
  return
664
687
 
@@ -684,7 +707,7 @@ class TPLinkMRClient(TPLinkMRClientBase):
684
707
  ]
685
708
  self.req_act(acts)
686
709
 
687
- def get_sms(self) -> [SMS]:
710
+ def get_sms(self) -> List[SMS]:
688
711
  acts = [
689
712
  self.ActItem(
690
713
  self.ActItem.SET, 'LTE_SMS_RECVMSGBOX', attrs=['PageNumber=1']),
@@ -57,7 +57,10 @@ class TPLinkMR200Client(TPLinkMRClient):
57
57
  ]
58
58
  _, values = self.req_act(acts)
59
59
 
60
- status.ipsecvpn_enable = values.get('enable') == '1'
60
+ if len(values) == 0:
61
+ status.ipsecvpn_enable = False
62
+ else:
63
+ status.ipsecvpn_enable = values.get('enable') == '1'
61
64
 
62
65
  return status
63
66