tplinkrouterc6u 5.0.3__py3-none-any.whl → 5.1.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,645 @@
1
+ from hashlib import md5
2
+ from re import search
3
+ from time import time, sleep
4
+ from urllib.parse import quote
5
+ from requests import Session
6
+ from datetime import timedelta, datetime
7
+ from macaddress import EUI48
8
+ from ipaddress import IPv4Address
9
+ from logging import Logger
10
+ from tplinkrouterc6u.common.encryption import EncryptionWrapperMR
11
+ from tplinkrouterc6u.common.package_enum import Connection
12
+ from tplinkrouterc6u.common.dataclass import (
13
+ Firmware,
14
+ Status,
15
+ Device,
16
+ IPv4Reservation,
17
+ IPv4DHCPLease,
18
+ IPv4Status,
19
+ SMS,
20
+ )
21
+ from tplinkrouterc6u.common.exception import ClientException, ClientError
22
+ from tplinkrouterc6u.client_abstract import AbstractRouter
23
+
24
+
25
+ class TPLinkMRClientBase(AbstractRouter):
26
+ REQUEST_RETRIES = 3
27
+
28
+ HEADERS = {
29
+ 'Accept': '*/*',
30
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0',
31
+ 'Referer': 'http://192.168.1.1/' # updated on the fly
32
+ }
33
+
34
+ HTTP_RET_OK = 0
35
+ HTTP_ERR_CGI_INVALID_ANSI = 71017
36
+ HTTP_ERR_USER_PWD_NOT_CORRECT = 71233
37
+ HTTP_ERR_USER_BAD_REQUEST = 71234
38
+
39
+ CLIENT_TYPES = {
40
+ 0: Connection.WIRED,
41
+ 1: Connection.HOST_2G,
42
+ 3: Connection.HOST_5G,
43
+ 2: Connection.GUEST_2G,
44
+ 4: Connection.GUEST_5G,
45
+ }
46
+
47
+ WIFI_SET = {
48
+ Connection.HOST_2G: '1,1,0,0,0,0',
49
+ Connection.HOST_5G: '1,2,0,0,0,0',
50
+ Connection.GUEST_2G: '1,1,1,0,0,0',
51
+ Connection.GUEST_5G: '1,2,1,0,0,0',
52
+ }
53
+
54
+ class ActItem:
55
+ GET = 1
56
+ SET = 2
57
+ ADD = 3
58
+ DEL = 4
59
+ GL = 5
60
+ GS = 6
61
+ OP = 7
62
+ CGI = 8
63
+
64
+ def __init__(self, type: int, oid: str, stack: str = '0,0,0,0,0,0', pstack: str = '0,0,0,0,0,0',
65
+ attrs: list = []):
66
+ self.type = type
67
+ self.oid = oid
68
+ self.stack = stack
69
+ self.pstack = pstack
70
+ self.attrs = attrs
71
+
72
+ def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
73
+ verify_ssl: bool = True, timeout: int = 30) -> None:
74
+ super().__init__(host, password, username, logger, verify_ssl, timeout)
75
+
76
+ self.req = Session()
77
+ self._token = None
78
+ self._hash = md5(f"{self.username}{self.password}".encode()).hexdigest()
79
+ self._nn = None
80
+ self._ee = None
81
+ self._seq = None
82
+ self._url_rsa_key = 'cgi/getParm'
83
+
84
+ self._encryption = EncryptionWrapperMR()
85
+
86
+ def supports(self) -> bool:
87
+ try:
88
+ self._req_rsa_key()
89
+ return True
90
+ except ClientException:
91
+ return False
92
+
93
+ def authorize(self) -> None:
94
+ '''
95
+ Establishes a login session to the host using provided credentials
96
+ '''
97
+ # hash the password
98
+
99
+ # request the RSA public key from the host
100
+ self._nn, self._ee, self._seq = self._req_rsa_key()
101
+
102
+ # authenticate
103
+ self._req_login()
104
+
105
+ # request TokenID
106
+ self._token = self._req_token()
107
+
108
+ def reboot(self) -> None:
109
+ acts = [
110
+ self.ActItem(self.ActItem.OP, 'ACT_REBOOT')
111
+ ]
112
+ self.req_act(acts)
113
+
114
+ def req_act(self, acts: list):
115
+ '''
116
+ Requests ACTs via the cgi_gdpr proxy
117
+ '''
118
+ act_types = []
119
+ act_data = []
120
+
121
+ for act in acts:
122
+ act_types.append(str(act.type))
123
+ act_data.append('[{}#{}#{}]{},{}\r\n{}\r\n'.format(
124
+ act.oid,
125
+ act.stack,
126
+ act.pstack,
127
+ len(act_types) - 1, # index, starts at 0
128
+ len(act.attrs),
129
+ '\r\n'.join(act.attrs)
130
+ ))
131
+
132
+ data = '&'.join(act_types) + '\r\n' + ''.join(act_data)
133
+
134
+ url = self._get_url('cgi_gdpr')
135
+ (code, response) = self._request(url, data_str=data, encrypt=True)
136
+
137
+ if code != 200:
138
+ error = 'TplinkRouter - MR - Response with error; Request {} - Response {}'.format(data, response)
139
+ if self._logger:
140
+ self._logger.debug(error)
141
+ raise ClientError(error)
142
+
143
+ result = self._merge_response(response)
144
+
145
+ return response, result.get('0') if len(result) == 1 and result.get('0') else result
146
+
147
+ @staticmethod
148
+ def _to_list(response: dict | list | None) -> list:
149
+ if response is None:
150
+ return []
151
+
152
+ return [response] if response.__class__ != list else response
153
+
154
+ @staticmethod
155
+ def _merge_response(response: str) -> dict:
156
+ result = {}
157
+ obj = {}
158
+ lines = response.split('\n')
159
+ for line in lines:
160
+ if line.startswith('['):
161
+ regexp = search(r'\[\d,\d,\d,\d,\d,\d\](\d)', line)
162
+ if regexp is not None:
163
+ obj = {}
164
+ index = regexp.group(1)
165
+ item = result.get(index)
166
+ if item is not None:
167
+ if item.__class__ != list:
168
+ result[index] = [item]
169
+ result[index].append(obj)
170
+ else:
171
+ result[index] = obj
172
+ continue
173
+ if '=' in line:
174
+ keyval = line.split('=')
175
+ assert len(keyval) == 2
176
+
177
+ obj[keyval[0]] = keyval[1]
178
+
179
+ return result if result else []
180
+
181
+ def _get_url(self, endpoint: str, params: dict = {}, include_ts: bool = True) -> str:
182
+ # add timestamp param
183
+ if include_ts:
184
+ params['_'] = str(round(time() * 1000))
185
+
186
+ # format params into a string
187
+ params_arr = []
188
+ for attr, value in params.items():
189
+ params_arr.append('{}={}'.format(attr, value))
190
+
191
+ # format url
192
+ return '{}/{}{}{}'.format(
193
+ self.host,
194
+ endpoint,
195
+ '?' if len(params_arr) > 0 else '',
196
+ '&'.join(params_arr)
197
+ )
198
+
199
+ def _req_token(self):
200
+ '''
201
+ Requests the TokenID, used for CGI authentication (together with cookies)
202
+ - token is inlined as JS var in the index (/) html page
203
+ e.g.: <script type="text/javascript">var token="086724f57013f16e042e012becf825";</script>
204
+
205
+ Return value:
206
+ TokenID string
207
+ '''
208
+ url = self._get_url('')
209
+ (code, response) = self._request(url, method='GET')
210
+ assert code == 200
211
+
212
+ result = search('var token="(.*)";', response)
213
+
214
+ assert result is not None
215
+ assert result.group(1) != ''
216
+
217
+ return result.group(1)
218
+
219
+ def _req_rsa_key(self):
220
+ '''
221
+ Requests the RSA public key from the host
222
+
223
+ Return value:
224
+ ((n, e), seq) tuple
225
+ '''
226
+ response = ''
227
+ try:
228
+ url = self._get_url(self._url_rsa_key)
229
+ (code, response) = self._request(url)
230
+ assert code == 200
231
+
232
+ # assert return code
233
+ assert self._parse_ret_val(response) == self.HTTP_RET_OK
234
+
235
+ # parse public key
236
+ ee = search('var ee="(.*)";', response)
237
+ nn = search('var nn="(.*)";', response)
238
+ seq = search('var seq="(.*)";', response)
239
+
240
+ assert ee and nn and seq
241
+ ee = ee.group(1)
242
+ nn = nn.group(1)
243
+ seq = seq.group(1)
244
+ assert len(ee) == 6
245
+ assert len(nn) == 128
246
+ assert seq.isnumeric()
247
+
248
+ except Exception as e:
249
+ error = ('TplinkRouter - {} - Unknown error rsa_key! Error - {}; Response - {}'
250
+ .format(self.__class__.__name__, e, response))
251
+ if self._logger:
252
+ self._logger.debug(error)
253
+ raise ClientException(error)
254
+
255
+ return nn, ee, int(seq)
256
+
257
+ def _req_login(self) -> None:
258
+ '''
259
+ Authenticates to the host
260
+ - sets the session token after successful login
261
+ - data/signature is passed as a GET parameter, NOT as a raw request data
262
+ (unlike for regular encrypted requests to the /cgi_gdpr endpoint)
263
+
264
+ Example session token (set as a cookie):
265
+ {'JSESSIONID': '4d786fede0164d7613411c7b6ec61e'}
266
+ '''
267
+ # encrypt username + password
268
+
269
+ sign, data = self._prepare_data(self.username + '\n' + self.password, True)
270
+ assert len(sign) == 256
271
+
272
+ data = {
273
+ 'data': quote(data, safe='~()*!.\''),
274
+ 'sign': sign,
275
+ 'Action': 1,
276
+ 'LoginStatus': 0,
277
+ 'isMobile': 0
278
+ }
279
+
280
+ url = self._get_url('cgi/login', data)
281
+ (code, response) = self._request(url)
282
+ assert code == 200
283
+
284
+ # parse and match return code
285
+ ret_code = self._parse_ret_val(response)
286
+ error = ''
287
+ if ret_code == self.HTTP_ERR_USER_PWD_NOT_CORRECT:
288
+ info = search('var currAuthTimes=(.*);\nvar currForbidTime=(.*);', response)
289
+ assert info is not None
290
+
291
+ error = 'TplinkRouter - MR - Login failed, wrong password. Auth times: {}/5, Forbid time: {}'.format(
292
+ info.group(1), info.group(2))
293
+ elif ret_code == self.HTTP_ERR_USER_BAD_REQUEST:
294
+ error = 'TplinkRouter - MR - Login failed. Generic error code: {}'.format(ret_code)
295
+ elif ret_code != self.HTTP_RET_OK:
296
+ error = 'TplinkRouter - MR - Login failed. Unknown error code: {}'.format(ret_code)
297
+
298
+ if error:
299
+ if self._logger:
300
+ self._logger.debug(error)
301
+ raise ClientException(error)
302
+
303
+ def _request(self, url, method='POST', data_str=None, encrypt=False):
304
+ '''
305
+ Prepares and sends an HTTP request to the host
306
+ - sets up the headers, handles token auth
307
+ - encrypts/decrypts the data, if needed
308
+
309
+ Return value:
310
+ (status_code, response_text) tuple
311
+ '''
312
+ headers = self.HEADERS
313
+
314
+ # add referer to request headers,
315
+ # otherwise we get 403 Forbidden
316
+ headers['Referer'] = self.host
317
+
318
+ # add token to request headers,
319
+ # used for CGI auth (together with JSESSIONID cookie)
320
+ if self._token is not None:
321
+ headers['TokenID'] = self._token
322
+
323
+ # encrypt request data if needed (for the /cgi_gdpr endpoint)
324
+ if encrypt:
325
+ sign, data = self._prepare_data(data_str, False)
326
+ data = 'sign={}\r\ndata={}\r\n'.format(sign, data)
327
+ else:
328
+ data = data_str
329
+
330
+ retry = 0
331
+ while retry < self.REQUEST_RETRIES:
332
+ # send the request
333
+ if method == 'POST':
334
+ r = self.req.post(url, data=data, headers=headers, timeout=self.timeout, verify=self._verify_ssl)
335
+ elif method == 'GET':
336
+ r = self.req.get(url, data=data, headers=headers, timeout=self.timeout, verify=self._verify_ssl)
337
+ else:
338
+ raise Exception('Unsupported method ' + str(method))
339
+
340
+ # sometimes we get 500 here, not sure why... just retry the request
341
+ if r.status_code != 500 and '<title>500 Internal Server Error</title>' not in r.text:
342
+ break
343
+
344
+ sleep(0.05)
345
+ retry += 1
346
+
347
+ # decrypt the response, if needed
348
+ if encrypt and (r.status_code == 200) and (r.text != ''):
349
+ return r.status_code, self._encryption.aes_decrypt(r.text)
350
+ else:
351
+ return r.status_code, r.text
352
+
353
+ def _parse_ret_val(self, response_text):
354
+ '''
355
+ Parses $.ret value from the response text
356
+
357
+ Return value:
358
+ return code (int)
359
+ '''
360
+ result = search(r'\$\.ret=(.*);', response_text)
361
+ assert result is not None
362
+ assert result.group(1).isnumeric()
363
+
364
+ return int(result.group(1))
365
+
366
+ def _prepare_data(self, data: str, is_login: bool) -> tuple[str, str]:
367
+ encrypted_data = self._encryption.aes_encrypt(data)
368
+ data_len = len(encrypted_data)
369
+ # get encrypted signature
370
+ signature = self._encryption.get_signature(int(self._seq) + data_len, is_login, self._hash, self._nn, self._ee)
371
+
372
+ # format expected raw request data
373
+ return signature, encrypted_data
374
+
375
+
376
+ class TPLinkMRClient(TPLinkMRClientBase):
377
+
378
+ def logout(self) -> None:
379
+ '''
380
+ Logs out from the host
381
+ '''
382
+ if self._token is None:
383
+ return
384
+
385
+ acts = [
386
+ # 8\r\n[/cgi/logout#0,0,0,0,0,0#0,0,0,0,0,0]0,0\r\n
387
+ self.ActItem(self.ActItem.CGI, '/cgi/logout')
388
+ ]
389
+
390
+ response, _ = self.req_act(acts)
391
+ ret_code = self._parse_ret_val(response)
392
+
393
+ if ret_code == self.HTTP_RET_OK:
394
+ self._token = None
395
+
396
+ def get_firmware(self) -> Firmware:
397
+ acts = [
398
+ self.ActItem(self.ActItem.GET, 'IGD_DEV_INFO', attrs=[
399
+ 'hardwareVersion',
400
+ 'modelName',
401
+ 'softwareVersion'
402
+ ])
403
+ ]
404
+ _, values = self.req_act(acts)
405
+
406
+ firmware = Firmware(values.get('hardwareVersion', ''), values.get('modelName', ''),
407
+ values.get('softwareVersion', ''))
408
+
409
+ return firmware
410
+
411
+ def get_status(self) -> Status:
412
+ status = Status()
413
+ acts = [
414
+ self.ActItem(self.ActItem.GS, 'LAN_IP_INTF', attrs=['X_TP_MACAddress', 'IPInterfaceIPAddress']),
415
+ self.ActItem(self.ActItem.GS, 'WAN_IP_CONN',
416
+ attrs=['enable', 'MACAddress', 'externalIPAddress', 'defaultGateway']),
417
+ self.ActItem(self.ActItem.GL, 'LAN_WLAN', attrs=['enable', 'X_TP_Band']),
418
+ self.ActItem(self.ActItem.GL, 'LAN_WLAN_GUESTNET', attrs=['enable', 'name']),
419
+ self.ActItem(self.ActItem.GL, 'LAN_HOST_ENTRY', attrs=[
420
+ 'IPAddress',
421
+ 'MACAddress',
422
+ 'hostName',
423
+ 'X_TP_ConnType',
424
+ 'active',
425
+ ]),
426
+ self.ActItem(self.ActItem.GS, 'LAN_WLAN_ASSOC_DEV', attrs=[
427
+ 'associatedDeviceMACAddress',
428
+ 'X_TP_TotalPacketsSent',
429
+ 'X_TP_TotalPacketsReceived',
430
+ ]),
431
+ ]
432
+ _, values = self.req_act(acts)
433
+
434
+ if values['0'].__class__ == list:
435
+ values['0'] = values['0'][0]
436
+
437
+ status._lan_macaddr = EUI48(values['0']['X_TP_MACAddress'])
438
+ status._lan_ipv4_addr = IPv4Address(values['0']['IPInterfaceIPAddress'])
439
+
440
+ for item in self._to_list(values.get('1')):
441
+ if int(item['enable']) == 0 and values.get('1').__class__ == list:
442
+ continue
443
+ status._wan_macaddr = EUI48(item['MACAddress']) if item.get('MACAddress') else None
444
+ status._wan_ipv4_addr = IPv4Address(item['externalIPAddress'])
445
+ status._wan_ipv4_gateway = IPv4Address(item['defaultGateway'])
446
+
447
+ if values['2'].__class__ != list:
448
+ status.wifi_2g_enable = bool(int(values['2']['enable']))
449
+ else:
450
+ status.wifi_2g_enable = bool(int(values['2'][0]['enable']))
451
+ status.wifi_5g_enable = bool(int(values['2'][1]['enable']))
452
+
453
+ if values['3'].__class__ != list:
454
+ status.guest_2g_enable = bool(int(values['3']['enable']))
455
+ else:
456
+ status.guest_2g_enable = bool(int(values['3'][0]['enable']))
457
+ status.guest_5g_enable = bool(int(values['3'][1]['enable']))
458
+
459
+ devices = {}
460
+ for val in self._to_list(values.get('4')):
461
+ if int(val['active']) == 0:
462
+ continue
463
+ conn = self.CLIENT_TYPES.get(int(val['X_TP_ConnType']))
464
+ if conn is None:
465
+ continue
466
+ elif conn == Connection.WIRED:
467
+ status.wired_total += 1
468
+ elif conn.is_guest_wifi():
469
+ status.guest_clients_total += 1
470
+ elif conn.is_host_wifi():
471
+ status.wifi_clients_total += 1
472
+ devices[val['MACAddress']] = Device(conn,
473
+ EUI48(val['MACAddress']),
474
+ IPv4Address(val['IPAddress']),
475
+ val['hostName'])
476
+
477
+ for val in self._to_list(values.get('5')):
478
+ if val['associatedDeviceMACAddress'] not in devices:
479
+ status.wifi_clients_total += 1
480
+ devices[val['associatedDeviceMACAddress']] = Device(
481
+ Connection.HOST_2G,
482
+ EUI48(val['associatedDeviceMACAddress']),
483
+ IPv4Address('0.0.0.0'),
484
+ '')
485
+ devices[val['associatedDeviceMACAddress']].packets_sent = int(val['X_TP_TotalPacketsSent'])
486
+ devices[val['associatedDeviceMACAddress']].packets_received = int(val['X_TP_TotalPacketsReceived'])
487
+
488
+ status.devices = list(devices.values())
489
+ status.clients_total = status.wired_total + status.wifi_clients_total + status.guest_clients_total
490
+
491
+ return status
492
+
493
+ def get_ipv4_reservations(self) -> [IPv4Reservation]:
494
+ acts = [
495
+ self.ActItem(self.ActItem.GL, 'LAN_DHCP_STATIC_ADDR', attrs=['enable', 'chaddr', 'yiaddr']),
496
+ ]
497
+ _, values = self.req_act(acts)
498
+
499
+ ipv4_reservations = []
500
+ for item in self._to_list(values):
501
+ ipv4_reservations.append(
502
+ IPv4Reservation(
503
+ EUI48(item['chaddr']),
504
+ IPv4Address(item['yiaddr']),
505
+ '',
506
+ bool(int(item['enable']))
507
+ ))
508
+
509
+ return ipv4_reservations
510
+
511
+ def get_ipv4_dhcp_leases(self) -> [IPv4DHCPLease]:
512
+ acts = [
513
+ self.ActItem(self.ActItem.GL, 'LAN_HOST_ENTRY', attrs=['IPAddress', 'MACAddress', 'hostName',
514
+ 'leaseTimeRemaining']),
515
+ ]
516
+ _, values = self.req_act(acts)
517
+
518
+ dhcp_leases = []
519
+ for item in self._to_list(values):
520
+ lease_time = item['leaseTimeRemaining']
521
+ dhcp_leases.append(
522
+ IPv4DHCPLease(
523
+ EUI48(item['MACAddress']),
524
+ IPv4Address(item['IPAddress']),
525
+ item['hostName'],
526
+ str(timedelta(seconds=int(lease_time))) if lease_time.isdigit() else 'Permanent',
527
+ ))
528
+
529
+ return dhcp_leases
530
+
531
+ def get_ipv4_status(self) -> IPv4Status:
532
+ acts = [
533
+ self.ActItem(self.ActItem.GS, 'LAN_IP_INTF',
534
+ attrs=['X_TP_MACAddress', 'IPInterfaceIPAddress', 'IPInterfaceSubnetMask']),
535
+ self.ActItem(self.ActItem.GET, 'LAN_HOST_CFG', '1,0,0,0,0,0', attrs=['DHCPServerEnable']),
536
+ self.ActItem(self.ActItem.GS, 'WAN_IP_CONN',
537
+ attrs=['enable', 'MACAddress', 'externalIPAddress', 'defaultGateway', 'name', 'subnetMask',
538
+ 'DNSServers']),
539
+ ]
540
+ _, values = self.req_act(acts)
541
+
542
+ ipv4_status = IPv4Status()
543
+ ipv4_status._lan_macaddr = EUI48(values['0']['X_TP_MACAddress'])
544
+ ipv4_status._lan_ipv4_ipaddr = IPv4Address(values['0']['IPInterfaceIPAddress'])
545
+ ipv4_status._lan_ipv4_netmask = IPv4Address(values['0']['IPInterfaceSubnetMask'])
546
+ ipv4_status.lan_ipv4_dhcp_enable = bool(int(values['1']['DHCPServerEnable']))
547
+
548
+ for item in self._to_list(values.get('2')):
549
+ if int(item['enable']) == 0 and values.get('2').__class__ == list:
550
+ continue
551
+ ipv4_status._wan_macaddr = EUI48(item['MACAddress'])
552
+ ipv4_status._wan_ipv4_ipaddr = IPv4Address(item['externalIPAddress'])
553
+ ipv4_status._wan_ipv4_gateway = IPv4Address(item['defaultGateway'])
554
+ ipv4_status.wan_ipv4_conntype = item['name']
555
+ ipv4_status._wan_ipv4_netmask = IPv4Address(item['subnetMask'])
556
+ dns = item['DNSServers'].split(',')
557
+ ipv4_status._wan_ipv4_pridns = IPv4Address(dns[0])
558
+ ipv4_status._wan_ipv4_snddns = IPv4Address(dns[1])
559
+
560
+ return ipv4_status
561
+
562
+ def set_wifi(self, wifi: Connection, enable: bool) -> None:
563
+ acts = [
564
+ self.ActItem(
565
+ self.ActItem.SET,
566
+ 'LAN_WLAN' if wifi in [Connection.HOST_2G, Connection.HOST_5G] else 'LAN_WLAN_MSSIDENTRY',
567
+ self.WIFI_SET[wifi],
568
+ attrs=['enable={}'.format(int(enable))]),
569
+ ]
570
+ self.req_act(acts)
571
+
572
+ def send_sms(self, phone_number: str, message: str) -> None:
573
+ acts = [
574
+ self.ActItem(
575
+ self.ActItem.SET, 'LTE_SMS_SENDNEWMSG', attrs=[
576
+ 'index=1',
577
+ 'to={}'.format(phone_number),
578
+ 'textContent={}'.format(message),
579
+ ]),
580
+ ]
581
+ self.req_act(acts)
582
+
583
+ def get_sms(self) -> [SMS]:
584
+ acts = [
585
+ self.ActItem(
586
+ self.ActItem.SET, 'LTE_SMS_RECVMSGBOX', attrs=['PageNumber=1']),
587
+ self.ActItem(
588
+ self.ActItem.GL, 'LTE_SMS_RECVMSGENTRY', attrs=['index', 'from', 'content', 'receivedTime',
589
+ 'unread']),
590
+ ]
591
+ _, values = self.req_act(acts)
592
+
593
+ messages = []
594
+ if values:
595
+ i = 1
596
+ for item in self._to_list(values.get('1')):
597
+ messages.append(
598
+ SMS(
599
+ i, item['from'], item['content'], datetime.fromisoformat(item['receivedTime']),
600
+ True if item['unread'] == '1' else False
601
+ )
602
+ )
603
+ i += 1
604
+
605
+ return messages
606
+
607
+ def set_sms_read(self, sms: SMS) -> None:
608
+ acts = [
609
+ self.ActItem(
610
+ self.ActItem.SET, 'LTE_SMS_RECVMSGENTRY', f'{sms.id},0,0,0,0,0', attrs=['unread=0']),
611
+ ]
612
+ self.req_act(acts)
613
+
614
+ def delete_sms(self, sms: SMS) -> None:
615
+ acts = [
616
+ self.ActItem(
617
+ self.ActItem.DEL, 'LTE_SMS_RECVMSGENTRY', f'{sms.id},0,0,0,0,0'),
618
+ ]
619
+ self.req_act(acts)
620
+
621
+ def send_ussd(self, command: str) -> str:
622
+ acts = [
623
+ self.ActItem(
624
+ self.ActItem.SET, 'LTE_USSD', attrs=[
625
+ 'action=1',
626
+ f"reqContent={command}",
627
+ ]),
628
+ ]
629
+ self.req_act(acts)
630
+
631
+ status = '0'
632
+ while status == '0':
633
+ sleep(1)
634
+ acts = [
635
+ self.ActItem(
636
+ self.ActItem.GET, 'LTE_USSD', attrs=['sessionStatus', 'sendResult', 'response', 'ussdStatus']),
637
+ ]
638
+ _, values = self.req_act(acts)
639
+
640
+ status = values.get('ussdStatus', '2')
641
+
642
+ if status == '1':
643
+ return values.get('response')
644
+ elif status == '2':
645
+ raise ClientError('Cannot send USSD!')
@@ -0,0 +1,48 @@
1
+ from requests.packages import urllib3
2
+ from logging import Logger
3
+ from tplinkrouterc6u.common.package_enum import Connection
4
+ from tplinkrouterc6u.common.dataclass import Firmware, Status
5
+ from abc import ABC, abstractmethod
6
+
7
+
8
+ class AbstractRouter(ABC):
9
+ def __init__(self, host: str, password: str, username: str = 'admin', logger: Logger = None,
10
+ verify_ssl: bool = True, timeout: int = 30) -> None:
11
+ self.username = username
12
+ self.password = password
13
+ self.timeout = timeout
14
+ self._logger = logger
15
+ self.host = host
16
+ if not (self.host.startswith('http://') or self.host.startswith('https://')):
17
+ self.host = "http://{}".format(self.host)
18
+ self._verify_ssl = verify_ssl
19
+ if self._verify_ssl is False:
20
+ urllib3.disable_warnings()
21
+
22
+ @abstractmethod
23
+ def supports(self) -> bool:
24
+ pass
25
+
26
+ @abstractmethod
27
+ def authorize(self) -> None:
28
+ pass
29
+
30
+ @abstractmethod
31
+ def logout(self) -> None:
32
+ pass
33
+
34
+ @abstractmethod
35
+ def get_firmware(self) -> Firmware:
36
+ pass
37
+
38
+ @abstractmethod
39
+ def get_status(self) -> Status:
40
+ pass
41
+
42
+ @abstractmethod
43
+ def reboot(self) -> None:
44
+ pass
45
+
46
+ @abstractmethod
47
+ def set_wifi(self, wifi: Connection, enable: bool) -> None:
48
+ pass
@@ -0,0 +1 @@
1
+ """All extra classes are here"""