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