tplinkrouterc6u 5.14.0__py3-none-any.whl → 5.15.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.
test/test_client_c6u.py CHANGED
@@ -603,6 +603,143 @@ class TestTPLinkClient(TestCase):
603
603
  self.assertEqual(status.devices[6].packets_sent, 134815)
604
604
  self.assertEqual(status.devices[6].packets_received, 2953078)
605
605
 
606
+ def test_get_status_with_game_accelerator_fallback_values(self) -> None:
607
+ response_status = '''
608
+ {
609
+ "success": true,
610
+ "data": {
611
+ "lan_macaddr": "06:e6:97:9e:23:f5",
612
+ "access_devices_wireless_host": [
613
+ {
614
+ "wire_type": "5G",
615
+ "macaddr": "f4:aa:bb:cc:dd:ee",
616
+ "ipaddr": "192.168.1.110",
617
+ "hostname": "CLIENT1"
618
+ }
619
+ ]
620
+ }
621
+ }
622
+ '''
623
+ response_game_accelerator = '''
624
+ {
625
+ "data": [
626
+ {"mac": "f4:aa:bb:cc:dd:ee", "deviceTag":"5G", "isGuest":false, "ip":"192.168.1.110",
627
+ "deviceName":"CLIENT1", "downSpeed":112458738, "upSpeed":704111,
628
+ "txRate":1441170, "rxRate":1080880, "online_time":34990.64,
629
+ "trafficUsed":19594852347, "signal": -60},
630
+ {"deviceName":"no-mac-device"}
631
+ ],
632
+ "timeout": false,
633
+ "success": true
634
+ }
635
+ '''
636
+ response_stats = '''
637
+ {
638
+ "data": [],
639
+ "timeout": false,
640
+ "success": true,
641
+ "operator": "load"
642
+ }
643
+ '''
644
+
645
+ router_class = self.router_class
646
+ game_accelerator_path = self.game_accelerator_path
647
+
648
+ class TPLinkRouterTest(router_class):
649
+ def request(self, path: str, data: str,
650
+ ignore_response: bool = False, ignore_errors: bool = False) -> dict | None:
651
+ if path == 'admin/status?form=all&operation=read':
652
+ return loads(response_status)['data']
653
+ elif path == game_accelerator_path:
654
+ return loads(response_game_accelerator)['data']
655
+ elif path == 'admin/wireless?form=statistics':
656
+ return loads(response_stats)['data']
657
+ raise ClientException()
658
+
659
+ client = TPLinkRouterTest('', '')
660
+ status = client.get_status()
661
+
662
+ self.assertEqual(len(status.devices), 1)
663
+ device = status.devices[0]
664
+ self.assertEqual(device.macaddr, 'F4-AA-BB-CC-DD-EE')
665
+ self.assertEqual(device.ipaddr, '192.168.1.110')
666
+ self.assertEqual(device.hostname, 'CLIENT1')
667
+ self.assertEqual(device.down_speed, 112458738)
668
+ self.assertEqual(device.up_speed, 704111)
669
+ self.assertEqual(device.tx_rate, 1441170)
670
+ self.assertEqual(device.rx_rate, 1080880)
671
+ self.assertEqual(device.online_time, 34990.64)
672
+ self.assertEqual(device.traffic_usage, 19594852347)
673
+ self.assertEqual(device.signal, -60)
674
+
675
+ def test_get_status_with_game_accelerator_alt_keys(self) -> None:
676
+ response_status = '''
677
+ {
678
+ "success": true,
679
+ "data": {
680
+ "lan_macaddr": "06:e6:97:9e:23:f5",
681
+ "access_devices_wireless_host": [
682
+ {
683
+ "wire_type": "5G",
684
+ "macaddr": "aa:bb:cc:dd:ee:ff",
685
+ "ipaddr": "192.168.1.120",
686
+ "hostname": "CLIENT2"
687
+ }
688
+ ]
689
+ }
690
+ }
691
+ '''
692
+ response_game_accelerator = '''
693
+ {
694
+ "data": [
695
+ {"mac": "aa:bb:cc:dd:ee:ff", "deviceTag":"5G", "isGuest":false, "ip":"192.168.1.120",
696
+ "deviceName":"CLIENT2", "downloadSpeed":12345, "uploadSpeed":2345,
697
+ "txrate":300, "rxrate":400, "onlineTime":123.45,
698
+ "trafficUsage":987654321, "signal": -55}
699
+ ],
700
+ "timeout": false,
701
+ "success": true
702
+ }
703
+ '''
704
+ response_stats = '''
705
+ {
706
+ "data": [],
707
+ "timeout": false,
708
+ "success": true,
709
+ "operator": "load"
710
+ }
711
+ '''
712
+
713
+ router_class = self.router_class
714
+ game_accelerator_path = self.game_accelerator_path
715
+
716
+ class TPLinkRouterTest(router_class):
717
+ def request(self, path: str, data: str,
718
+ ignore_response: bool = False, ignore_errors: bool = False) -> dict | None:
719
+ if path == 'admin/status?form=all&operation=read':
720
+ return loads(response_status)['data']
721
+ elif path == game_accelerator_path:
722
+ return loads(response_game_accelerator)['data']
723
+ elif path == 'admin/wireless?form=statistics':
724
+ return loads(response_stats)['data']
725
+ raise ClientException()
726
+
727
+ client = TPLinkRouterTest('', '')
728
+ status = client.get_status()
729
+
730
+ self.assertEqual(len(status.devices), 1)
731
+ device = status.devices[0]
732
+ self.assertEqual(device.macaddr, 'AA-BB-CC-DD-EE-FF')
733
+ self.assertEqual(device.ipaddr, '192.168.1.120')
734
+ self.assertEqual(device.hostname, 'CLIENT2')
735
+ self.assertEqual(device.down_speed, 12345)
736
+ self.assertEqual(device.up_speed, 2345)
737
+ self.assertEqual(device.tx_rate, 300)
738
+ self.assertEqual(device.rx_rate, 400)
739
+ self.assertEqual(device.online_time, 123.45)
740
+ self.assertEqual(device.traffic_usage, 987654321)
741
+ self.assertEqual(device.signal, -55)
742
+
606
743
  def test_get_status_with_perf_request(self) -> None:
607
744
  response_status = '''
608
745
  {
test/test_client_ex.py CHANGED
@@ -6,6 +6,7 @@ from tplinkrouterc6u import (
6
6
  Connection,
7
7
  Firmware,
8
8
  Status,
9
+ LTEStatus,
9
10
  Device,
10
11
  IPv4Reservation,
11
12
  IPv4DHCPLease,
@@ -705,6 +706,76 @@ class TestTPLinkEXClient(TestCase):
705
706
  self.assertEqual(check_data, '{"data":{"stack":"0,0,0,0,0,0","pstack":"0,0,0,0,0,0",'
706
707
  '"enable":"1"},"operation":"so","oid":"DEV2_OPENVPN"}')
707
708
 
709
+ def test_get_lte_status(self) -> None:
710
+
711
+ DEV2_LTE_LINK_CFG = ('{"data":{"enable":"1","wispConnStat":"0","networkType":"3","endcStatus":"-1",'
712
+ '"connectedBand":"B3","availableBand":"B3","roamingStatus":"0","connectStatus":"4",'
713
+ '"simStatus":"3","simCardNumber":"0","simCardType":"0","simCardState":"1",'
714
+ '"simCardImsi":"0","simCardGid1":"010XXXFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",'
715
+ '"dataSwitch":"1","roamSwitch":"0","prefNet":"3","timeFactroyFlag":"0",'
716
+ '"wlanFactoryFlag":"0","smsScEnable":"0","smsScAddress":"491712121212",'
717
+ '"signalStrength":"0","ipv4":"10.171.53.12","dns1v4":"62.109.121.17",'
718
+ '"dns2v4":"62.109.121.18","ipv4_mtu":"1500","ipv6":"2a02:3036:282:7376",'
719
+ '"ipv6PrefixLen":"64","dns1v6":"2a02:3018:0","dns2v6":"2a02:3018:0:40ff::bbbb",'
720
+ '"ipv6_mtu":"1500","ifName":"rmnet_data0","gatewayV4":"10.171.53.2",'
721
+ '"gatewayV6":"fe80::4d7c:fa07","netmask":"255.255.255.248","__indexPrimaryServingCell":"",'
722
+ '"stack":"1,0,0,0,0,0"},"operation":"go","oid":"DEV2_LTE_LINK_CFG","success":true}')
723
+ DEV2_XTP_LTE_INTF_CFG = ('{"data":{"enable":"1","freeDurationEnabled":"0","freeDurationWhenRoaming":"0",'
724
+ '"freeDurationStart":"00:00","freeDurationEnd":"00:00","cfgModified":"0",'
725
+ '"dailyFlow":"294896362.0000","currentDate":"1770073200","ledLimitReached":"1",'
726
+ '"allowDialOnce":"0","enableDataLimit":"0","autoDisconnect":"1","dataLimit":"0",'
727
+ '"limitType":"0","limitation":"0","warningPercent":"90","warnSimNumber":"",'
728
+ '"needSmsTip":"0","enablePaymentDay":"1","nextDue":"1772233200","paymentDay":"28",'
729
+ '"adjustStatistics":"-1","curStatistics":"0","curConnTime":"1545647",'
730
+ '"totalStatistics":"38819729967.0000","totalConnTime":"84667","rxFlow":"0",'
731
+ '"txFlow":"0","curRxSpeed":"31921","curTxSpeed":"28242","plmnLock":"0",'
732
+ '"enablePushNotification":"0","stack":"1,0,0,0,0,0"},"operation":"go",'
733
+ '"oid":"DEV2_XTP_LTE_INTF_CFG","success":true}')
734
+ DEV2_LTE_NET_STATUS = ('{"data":{"ussdSessionStatus":"0","ussdStatus":"0","smsUnreadCount":"0",'
735
+ '"smsSendCause":"0","smsSendResult":"3","rfSwitch":"1","sigLevel":"0","connStat":"4",'
736
+ '"roamStat":"0","regStat":"1","netType":"3","srvStat":"2","rfInfoChannel":"0",'
737
+ '"rfInfoBand":"0","rfInfoIf":"0","rfInfoRat":"0","rfInfoRssi":"0","rfInfoRsrp":"0",'
738
+ '"rfInfoRsrq":"0","rfInfoSnr":"0","rfInfoEcio":"0","region":"","callStatus":"0",'
739
+ '"stack":"1,0,0,0,0,0"},"operation":"go","oid":"DEV2_LTE_NET_STATUS","success":true}')
740
+ DEV2_LTE_PROF_STAT = ('{"data":{"activeProfType":"0","activeProfIndex":"0","spn":"","ispWhich":"0",'
741
+ '"ispCount":"3","ispMnc":"3","ispMcc":"262","ispName":"O2 2025","usrWhich":"0",'
742
+ '"usrCount":"1","usrMnc":"3","usrMcc":"262","stack":"1,0,0,0,0,0"},"operation":"go",'
743
+ '"oid":"DEV2_LTE_PROF_STAT","success":true}')
744
+
745
+ class TPLinkEXClientTest(TPLinkEXClient):
746
+ self._token = True
747
+
748
+ def _request(self, url, method='POST', data_str=None, encrypt=False):
749
+ if 'DEV2_LTE_LINK_CFG' in data_str:
750
+ return 200, DEV2_LTE_LINK_CFG
751
+ elif 'DEV2_XTP_LTE_INTF_CFG' in data_str:
752
+ return 200, DEV2_XTP_LTE_INTF_CFG
753
+ elif 'DEV2_LTE_NET_STATUS' in data_str:
754
+ return 200, DEV2_LTE_NET_STATUS
755
+ elif 'DEV2_LTE_PROF_STAT' in data_str:
756
+ return 200, DEV2_LTE_PROF_STAT
757
+ raise ClientException()
758
+
759
+ client = TPLinkEXClientTest('', '')
760
+ status = client.get_lte_status()
761
+
762
+ self.assertIsInstance(status, LTEStatus)
763
+
764
+ self.assertIsInstance(status, LTEStatus)
765
+ self.assertEqual(status.enable, 1)
766
+ self.assertEqual(status.connect_status, 4)
767
+ self.assertEqual(status.network_type, 3)
768
+ self.assertEqual(status.sim_status, 3)
769
+ self.assertEqual(status.sig_level, 0)
770
+ self.assertEqual(status.total_statistics, 38819729967)
771
+ self.assertEqual(status.cur_rx_speed, 31921)
772
+ self.assertEqual(status.cur_tx_speed, 28242)
773
+ self.assertEqual(status.sms_unread_count, 0)
774
+ self.assertEqual(status.rsrp, 0)
775
+ self.assertEqual(status.rsrq, 0)
776
+ self.assertEqual(status.snr, 0)
777
+ self.assertEqual(status.isp_name, 'O2 2025')
778
+
708
779
 
709
780
  if __name__ == '__main__':
710
781
  main()
test/test_client_sg.py ADDED
@@ -0,0 +1,217 @@
1
+ import json
2
+ from hashlib import sha256
3
+ from unittest import main, TestCase
4
+ from unittest.mock import patch, Mock
5
+
6
+ from tplinkrouterc6u import TplinkRouterSG, ClientException
7
+ from test_client_c6u import TestTPLinkClient
8
+
9
+
10
+ class TestTPLinkClientSG(TestTPLinkClient):
11
+ """Inherits get_status and other tests from TestTPLinkClient."""
12
+
13
+ router_class = TplinkRouterSG
14
+ game_accelerator_path = 'admin/smart_network?form=game_accelerator&operation=loadDevice'
15
+ openvpn_config_path = 'admin/openvpn?form=config&operation=read'
16
+ pptpd_config_path = 'admin/pptpd?form=config&operation=read'
17
+ vpn_uses_data_param = False
18
+
19
+
20
+ class TestTplinkRouterSGUnit(TestCase):
21
+ """Unit tests specific to TplinkRouterSG authentication and encryption."""
22
+
23
+ def test_supports_password_too_long(self) -> None:
24
+ long_password = 'a' * 126
25
+ client = TplinkRouterSG('http://192.168.0.1', long_password)
26
+ self.assertFalse(client.supports())
27
+
28
+ @patch('tplinkrouterc6u.client.sg.post')
29
+ def test_check_sg_certification_match(self, mock_post: Mock) -> None:
30
+ response = Mock()
31
+ response.json.return_value = {
32
+ 'data': {
33
+ 'certification': ['SG CLS L1 STAGE2', 'OTHER']
34
+ }
35
+ }
36
+ mock_post.return_value = response
37
+
38
+ client = TplinkRouterSG('http://192.168.0.1', 'testpassword')
39
+ result = client._check_sg_certification()
40
+
41
+ self.assertTrue(result)
42
+ self.assertEqual(mock_post.call_count, 1)
43
+ call_args = mock_post.call_args
44
+ self.assertIn('device_config', call_args[0][0])
45
+
46
+ @patch('tplinkrouterc6u.client.sg.post')
47
+ def test_check_sg_certification_no_match(self, mock_post: Mock) -> None:
48
+ response = Mock()
49
+ response.json.return_value = {
50
+ 'data': {
51
+ 'certification': ['SOME_OTHER_CERT']
52
+ }
53
+ }
54
+ mock_post.return_value = response
55
+
56
+ client = TplinkRouterSG('http://192.168.0.1', 'testpassword')
57
+ result = client._check_sg_certification()
58
+
59
+ self.assertFalse(result)
60
+
61
+ @patch('tplinkrouterc6u.client.sg.post')
62
+ def test_authorize_success(self, mock_post: Mock) -> None:
63
+ pwd_keys_response = Mock()
64
+ pwd_keys_response.json.return_value = {
65
+ 'data': {
66
+ 'password': ['mock_pwd_nn', '010001']
67
+ }
68
+ }
69
+
70
+ auth_keys_response = Mock()
71
+ auth_keys_response.json.return_value = {
72
+ 'data': {
73
+ 'seq': 100,
74
+ 'key': ['mock_auth_nn', '010001']
75
+ }
76
+ }
77
+
78
+ login_response = Mock()
79
+ login_response.json.return_value = {'data': 'encrypted_login_blob'}
80
+ login_response.headers = {
81
+ 'set-cookie': 'sysauth=test_sysauth_value; path=/'
82
+ }
83
+ login_response.text = 'mock response text'
84
+
85
+ mock_post.side_effect = [
86
+ pwd_keys_response,
87
+ auth_keys_response,
88
+ login_response,
89
+ ]
90
+
91
+ client = TplinkRouterSG('http://192.168.0.1', 'testpassword')
92
+
93
+ login_result = json.dumps({
94
+ 'success': True,
95
+ 'data': {'stok': 'test_stok_12345'}
96
+ })
97
+ with patch.object(client, '_rsa_v15_encrypt', return_value='encrypted_pwd_hex'), \
98
+ patch.object(client, '_aes_encrypt', return_value='encrypted_data_b64'), \
99
+ patch.object(client, '_build_login_signature', return_value='mock_sign'), \
100
+ patch.object(client, '_aes_decrypt', return_value=login_result):
101
+ client.authorize()
102
+
103
+ self.assertTrue(client._logged)
104
+ self.assertEqual(client._stok, 'test_stok_12345')
105
+ self.assertEqual(client._sysauth, 'test_sysauth_value')
106
+ self.assertEqual(mock_post.call_count, 3)
107
+
108
+ first_call = mock_post.call_args_list[0]
109
+ self.assertIn('login?form=keys', first_call[0][0])
110
+
111
+ second_call = mock_post.call_args_list[1]
112
+ self.assertIn('login?form=auth', second_call[0][0])
113
+
114
+ third_call = mock_post.call_args_list[2]
115
+ self.assertIn('login?form=login', third_call[0][0])
116
+
117
+ @patch('tplinkrouterc6u.client.sg.post')
118
+ def test_authorize_failure(self, mock_post: Mock) -> None:
119
+ pwd_keys_response = Mock()
120
+ pwd_keys_response.json.return_value = {
121
+ 'data': {
122
+ 'password': ['mock_pwd_nn', '010001']
123
+ }
124
+ }
125
+
126
+ auth_keys_response = Mock()
127
+ auth_keys_response.json.return_value = {
128
+ 'data': {
129
+ 'seq': 100,
130
+ 'key': ['mock_auth_nn', '010001']
131
+ }
132
+ }
133
+
134
+ login_response = Mock()
135
+ login_response.json.return_value = {'data': 'encrypted_login_blob'}
136
+ login_response.headers = {}
137
+ login_response.text = 'mock error response'
138
+
139
+ mock_post.side_effect = [
140
+ pwd_keys_response,
141
+ auth_keys_response,
142
+ login_response,
143
+ ]
144
+
145
+ client = TplinkRouterSG('http://192.168.0.1', 'wrongpassword')
146
+
147
+ login_result = json.dumps({
148
+ 'success': False,
149
+ 'data': {'errorcode': 'invalid password'}
150
+ })
151
+ with patch.object(client, '_rsa_v15_encrypt', return_value='encrypted_pwd_hex'), \
152
+ patch.object(client, '_aes_encrypt', return_value='encrypted_data_b64'), \
153
+ patch.object(client, '_build_login_signature', return_value='mock_sign'), \
154
+ patch.object(client, '_aes_decrypt', return_value=login_result):
155
+ with self.assertRaises(ClientException) as context:
156
+ client.authorize()
157
+
158
+ self.assertIn('Login failed', str(context.exception))
159
+ self.assertFalse(client._logged)
160
+
161
+ def test_authorize_uses_username(self) -> None:
162
+ """Verify SHA256 hash uses self.username, not hardcoded 'admin'."""
163
+ client = TplinkRouterSG(
164
+ 'http://192.168.0.1', 'testpassword', username='customuser')
165
+
166
+ expected_hash = sha256(
167
+ ('customuser' + 'testpassword').encode()).hexdigest()
168
+ admin_hash = sha256(
169
+ ('admin' + 'testpassword').encode()).hexdigest()
170
+
171
+ # Simulate the hash computation that happens in authorize()
172
+ client._hash = sha256(
173
+ (client.username + client.password).encode()).hexdigest()
174
+
175
+ self.assertEqual(client._hash, expected_hash)
176
+ self.assertNotEqual(client._hash, admin_hash)
177
+
178
+ @patch('tplinkrouterc6u.client.sg.post')
179
+ def test_request_hmac_signature(self, mock_post: Mock) -> None:
180
+ """Verify non-login requests use HMAC-SHA256 signature."""
181
+ client = TplinkRouterSG('http://192.168.0.1', 'testpassword')
182
+ client._logged = True
183
+ client._stok = 'test_stok'
184
+ client._sysauth = 'test_sysauth'
185
+ client._aes_key = '1234567890123456'
186
+ client._aes_iv = '6543210987654321'
187
+ client._hash = 'fakehash'
188
+ client._seq = 100
189
+
190
+ response = Mock()
191
+ decrypted_data = json.dumps({
192
+ 'success': True,
193
+ 'data': {'key': 'value'}
194
+ })
195
+ response.json.return_value = {'data': 'encrypted'}
196
+
197
+ mock_post.return_value = response
198
+
199
+ with patch.object(
200
+ client, '_aes_decrypt', return_value=decrypted_data
201
+ ):
202
+ result = client.request(
203
+ 'admin/status?form=all', 'operation=read')
204
+
205
+ self.assertEqual(result, {'key': 'value'})
206
+
207
+ call_kwargs = mock_post.call_args
208
+ body = call_kwargs[1]['data']
209
+ self.assertTrue(body.startswith('sign='))
210
+ self.assertIn('&data=', body)
211
+
212
+ # Hash should have been updated to SHA256 of the encrypted data
213
+ self.assertNotEqual(client._hash, 'fakehash')
214
+
215
+
216
+ if __name__ == '__main__':
217
+ main()
@@ -1,4 +1,5 @@
1
1
  from tplinkrouterc6u.client.c6u import TplinkRouter, TplinkRouterV1_11
2
+ from tplinkrouterc6u.client.sg import TplinkRouterSG
2
3
  from tplinkrouterc6u.client.deco import TPLinkDecoClient
3
4
  from tplinkrouterc6u.client_abstract import AbstractRouter
4
5
  from tplinkrouterc6u.client.mr import TPLinkMRClient, TPLinkMRClientGCM
@@ -348,18 +348,27 @@ class TplinkBaseRouter(AbstractRouter, TplinkRequest):
348
348
 
349
349
  if smart_network:
350
350
  for item in smart_network:
351
- if item['mac'] not in devices:
351
+ mac = item.get('mac')
352
+ if not mac:
353
+ continue
354
+
355
+ if mac not in devices:
352
356
  conn = self._map_wire_type(item.get('deviceTag'), not item.get('isGuest'))
353
- devices[item['mac']] = Device(conn, get_mac(item.get('mac', '00:00:00:00:00:00')),
354
- get_ip(item['ip']), item['deviceName'])
357
+ devices[mac] = Device(conn, get_mac(item.get('mac', '00:00:00:00:00:00')),
358
+ get_ip(item.get('ip', '0.0.0.0')), item.get('deviceName', ''))
355
359
  if conn.is_iot():
356
360
  if status.iot_clients_total is None:
357
361
  status.iot_clients_total = 0
358
362
  status.iot_clients_total += 1
359
363
 
360
- devices[item['mac']].down_speed = item.get('downloadSpeed')
361
- devices[item['mac']].up_speed = item.get('uploadSpeed')
362
- devices[item['mac']].signal = int(item.get('signal')) if item.get('signal') else None
364
+ device = devices[mac]
365
+ device.down_speed = item.get('downloadSpeed', item.get('downSpeed'))
366
+ device.up_speed = item.get('uploadSpeed', item.get('upSpeed'))
367
+ device.tx_rate = item.get('txrate', item.get('txRate'))
368
+ device.rx_rate = item.get('rxrate', item.get('rxRate'))
369
+ device.online_time = item.get('onlineTime', item.get('online_time'))
370
+ device.traffic_usage = item.get('trafficUsage', item.get('trafficUsed'))
371
+ device.signal = int(item.get('signal')) if item.get('signal') else None
363
372
 
364
373
  try:
365
374
  wireless_stats = self.request('admin/wireless?form=statistics', 'operation=load')
@@ -514,7 +523,10 @@ class TplinkRouterV1_11(TplinkBaseRouter):
514
523
  try:
515
524
  self._request_pwd()
516
525
  # V1_11 uses 2048-bit RSA = 512 hex chars, older firmware uses 1024-bit = 256 chars
517
- return len(self._pwdNN) >= 512
526
+ if len(self._pwdNN) >= 512:
527
+ self.authorize()
528
+ self.logout()
529
+ return True
518
530
  except Exception:
519
531
  return False
520
532
 
@@ -534,7 +546,7 @@ class TplinkRouterV1_11(TplinkBaseRouter):
534
546
  self._pwdNN = data[self._data_block]['password'][0]
535
547
  self._pwdEE = data[self._data_block]['password'][1]
536
548
  except Exception as e:
537
- error = ('TplinkRouter - {} - Failed to get encryption keys! Error - {}; Response - {}'
549
+ error = ('TplinkRouterV1_11 - {} - Failed to get encryption keys! Error - {}; Response - {}'
538
550
  .format(self.__class__.__name__, e, response.text))
539
551
  if self._logger:
540
552
  self._logger.debug(error)
@@ -563,7 +575,7 @@ class TplinkRouterV1_11(TplinkBaseRouter):
563
575
  if not data.get('success'):
564
576
  error_info = data.get(self._data_block, {})
565
577
  raise ClientException(
566
- 'TplinkRouter - {} - Login failed: {}'.format(
578
+ 'TplinkRouterV1_11 - {} - Login failed: {}'.format(
567
579
  self.__class__.__name__,
568
580
  error_info.get('errorcode', 'unknown error')
569
581
  )
@@ -582,7 +594,7 @@ class TplinkRouterV1_11(TplinkBaseRouter):
582
594
  except ClientException:
583
595
  raise
584
596
  except Exception as e:
585
- error = ('TplinkRouter - {} - Cannot authorize! Error - {}; Response - {}'
597
+ error = ('TplinkRouterV1_11 - {} - Cannot authorize! Error - {}; Response - {}'
586
598
  .format(self.__class__.__name__, e, response.text))
587
599
  if self._logger:
588
600
  self._logger.debug(error)
@@ -13,6 +13,7 @@ from tplinkrouterc6u.common.dataclass import (
13
13
  IPv4Reservation,
14
14
  IPv4DHCPLease,
15
15
  IPv4Status,
16
+ LTEStatus,
16
17
  VPNStatus)
17
18
  from tplinkrouterc6u.common.exception import ClientException, ClientError
18
19
  from tplinkrouterc6u.client.mr import TPLinkMRClientBase, TPLinkMRClientBaseGCM
@@ -238,6 +239,38 @@ class TPLinkEXClient(TPLinkMRClientBase):
238
239
  ]
239
240
  self.req_act(acts)
240
241
 
242
+ def get_lte_status(self) -> LTEStatus:
243
+ status = LTEStatus()
244
+ acts = [
245
+ self.ActItem(self.ActItem.GET, 'DEV2_LTE_LINK_CFG', '1,0,0,0,0,0',
246
+ attrs=['enable', 'connectStatus', 'networkType', 'simStatus']),
247
+ self.ActItem(self.ActItem.GET, 'DEV2_XTP_LTE_INTF_CFG', '1,0,0,0,0,0',
248
+ attrs=['totalStatistics', 'curRxSpeed', 'curTxSpeed']),
249
+ self.ActItem(self.ActItem.GET, 'DEV2_LTE_NET_STATUS', '1,0,0,0,0,0',
250
+ attrs=['smsUnreadCount', 'sigLevel', 'rfInfoRsrp', 'rfInfoRsrq', 'rfInfoSnr']),
251
+ self.ActItem(self.ActItem.GET, 'DEV2_LTE_PROF_STAT', '1,0,0,0,0,0', attrs=['ispName']),
252
+ ]
253
+ _, values = self.req_act(acts)
254
+
255
+ status.enable = int(values[0]['enable'])
256
+ status.connect_status = int(values[0]['connectStatus'])
257
+ status.network_type = int(values[0]['networkType'])
258
+ status.sim_status = int(values[0]['simStatus'])
259
+
260
+ status.total_statistics = int(float(values[1]['totalStatistics']))
261
+ status.cur_rx_speed = int(values[1]['curRxSpeed'])
262
+ status.cur_tx_speed = int(values[1]['curTxSpeed'])
263
+
264
+ status.sms_unread_count = int(values[2]['smsUnreadCount'])
265
+ status.sig_level = int(values[2]['sigLevel'])
266
+ status.rsrp = int(values[2]['rfInfoRsrp'])
267
+ status.rsrq = int(values[2]['rfInfoRsrq'])
268
+ status.snr = int(values[2]['rfInfoSnr'])
269
+
270
+ status.isp_name = values[3]['ispName']
271
+
272
+ return status
273
+
241
274
  def req_act(self, acts: list):
242
275
  '''
243
276
  Requests ACTs via the cgi_gdpr proxy
@@ -91,7 +91,8 @@ class TPLinkMR200Client(TPLinkMRClient):
91
91
  status = LTEStatus()
92
92
  acts = [
93
93
  self.ActItem(self.ActItem.GET, 'WAN_LTE_LINK_CFG', '2,1,0,0,0,0',
94
- attrs=['enable', 'connectStatus', 'networkType', 'roamingStatus', 'simStatus']),
94
+ attrs=['enable', 'connectStatus', 'networkType', 'roamingStatus', 'simStatus',
95
+ 'signalStrength']),
95
96
  self.ActItem(self.ActItem.GET, 'WAN_LTE_INTF_CFG', '2,0,0,0,0,0',
96
97
  attrs=['dataLimit', 'enablePaymentDay', 'curStatistics', 'totalStatistics', 'enableDataLimit',
97
98
  'limitation',
@@ -0,0 +1,285 @@
1
+ """
2
+ TP-Link router client for SG_L1_S2 / CE_RED certified devices.
3
+
4
+ Routers with these certifications (e.g., Archer BE3600, Wi-Fi 7 models)
5
+ use an enhanced encryption scheme:
6
+ 1. SHA256 hash instead of MD5 for authentication
7
+ 2. PKCS1-OAEP RSA padding for login signature encryption
8
+ 3. HMAC-SHA256 for non-login request signatures
9
+ 4. Dynamic hash replacement: SHA256(encrypted_data) per request
10
+
11
+ Fixes: https://github.com/AlexandrErohin/home-assistant-tplink-router/issues/220
12
+ """
13
+
14
+ import hmac
15
+ import json
16
+ from re import search
17
+ from hashlib import sha256
18
+ from base64 import b64encode, b64decode
19
+ from random import randint
20
+ from logging import Logger
21
+ from urllib.parse import quote
22
+
23
+ from Crypto.PublicKey.RSA import construct
24
+ from Crypto.Cipher import PKCS1_OAEP, PKCS1_v1_5, AES
25
+ from Crypto.Util.Padding import pad, unpad
26
+ from binascii import hexlify
27
+ from requests import post
28
+
29
+ from tplinkrouterc6u.client.c6u import TplinkBaseRouter
30
+ from tplinkrouterc6u.common.exception import ClientException, ClientError
31
+
32
+ # SG_L1_S2 and CE_RED certifications that trigger enhanced encryption
33
+ SG_CERTIFICATIONS = ['SG CLS L1 STAGE2', 'EU CE RED']
34
+
35
+ SIGNATURE_OFFSET = 53
36
+ AES_KEY_LEN = 16
37
+
38
+
39
+ class TplinkRouterSG(TplinkBaseRouter):
40
+ """
41
+ Client for TP-Link routers with SG_L1_S2 / CE_RED certification.
42
+
43
+ These routers use SHA256 + OAEP + HMAC-SHA256 instead of the standard
44
+ MD5 + PKCS1_v1_5 + RSA encryption scheme.
45
+ """
46
+
47
+ def __init__(self, host: str, password: str, username: str = 'admin',
48
+ logger: Logger = None, verify_ssl: bool = True,
49
+ timeout: int = 30) -> None:
50
+ super().__init__(host, password, username, logger, verify_ssl, timeout)
51
+ self._aes_key = ''
52
+ self._aes_iv = ''
53
+ self._hash = ''
54
+ self._seq = 0
55
+ self._nn = ''
56
+ self._ee = ''
57
+ self._pwdNN = ''
58
+ self._pwdEE = ''
59
+ self._data_block = 'data'
60
+
61
+ def supports(self) -> bool:
62
+ """Check if this router uses SG/CE_RED encryption."""
63
+ if len(self.password) > 125:
64
+ return False
65
+ try:
66
+ if not self._check_sg_certification():
67
+ return False
68
+ self._request_pwd_keys()
69
+ return True
70
+ except Exception:
71
+ return False
72
+
73
+ def _check_sg_certification(self) -> bool:
74
+ """Check if the router has SG_L1_S2 or CE_RED certification."""
75
+ url = '{}/cgi-bin/luci/;stok=/device_config?form=config'.format(self.host)
76
+ response = post(
77
+ url, data='operation=read',
78
+ headers=self._headers_login,
79
+ timeout=self.timeout, verify=self._verify_ssl,
80
+ )
81
+ try:
82
+ data = response.json()
83
+ certs = data.get('data', {}).get('certification', [])
84
+ return any(c in SG_CERTIFICATIONS for c in certs)
85
+ except Exception:
86
+ return False
87
+
88
+ def _generate_aes_key(self) -> None:
89
+ """Generate random AES key and IV (16 random digits, matching JS behavior)."""
90
+ self._aes_key = ''.join([str(randint(0, 9)) for _ in range(AES_KEY_LEN)])
91
+ self._aes_iv = ''.join([str(randint(0, 9)) for _ in range(AES_KEY_LEN)])
92
+
93
+ def _aes_encrypt(self, data: str) -> str:
94
+ """AES-CBC encrypt with PKCS7 padding."""
95
+ cipher = AES.new(self._aes_key.encode(), AES.MODE_CBC, self._aes_iv.encode())
96
+ return b64encode(cipher.encrypt(pad(data.encode(), AES.block_size))).decode()
97
+
98
+ def _aes_decrypt(self, data: str) -> str:
99
+ """AES-CBC decrypt with PKCS7 unpadding."""
100
+ cipher = AES.new(self._aes_key.encode(), AES.MODE_CBC, self._aes_iv.encode())
101
+ return unpad(cipher.decrypt(b64decode(data)), AES.block_size).decode()
102
+
103
+ @staticmethod
104
+ def _rsa_v15_encrypt(data: str, n_hex: str, e_hex: str) -> str:
105
+ """RSA encrypt with PKCS1 v1.5 padding (used for password encryption)."""
106
+ key = construct((int(n_hex, 16), int(e_hex, 16)))
107
+ cipher = PKCS1_v1_5.new(key)
108
+ result = hexlify(cipher.encrypt(data.encode())).decode()
109
+ key_len = len(n_hex)
110
+ return result.zfill(key_len) if len(result) < key_len else result
111
+
112
+ @staticmethod
113
+ def _rsa_oaep_encrypt(data: str, n_hex: str, e_hex: str) -> str:
114
+ """RSA encrypt with PKCS1-OAEP padding (used for login signature)."""
115
+ key = construct((int(n_hex, 16), int(e_hex, 16)))
116
+ cipher = PKCS1_OAEP.new(key)
117
+ result = hexlify(cipher.encrypt(data.encode())).decode()
118
+ key_len = len(n_hex)
119
+ return result.zfill(key_len) if len(result) < key_len else result
120
+
121
+ def _get_aes_formatted_key(self) -> str:
122
+ return 'k={}&i={}'.format(self._aes_key, self._aes_iv)
123
+
124
+ def _request_pwd_keys(self) -> None:
125
+ """Get RSA public key for password encryption."""
126
+ url = '{}/cgi-bin/luci/;stok=/login?form=keys'.format(self.host)
127
+ response = post(
128
+ url, params={'operation': 'read'},
129
+ timeout=self.timeout, verify=self._verify_ssl,
130
+ )
131
+ try:
132
+ data = response.json()
133
+ self._pwdNN = data['data']['password'][0]
134
+ self._pwdEE = data['data']['password'][1]
135
+ except Exception as e:
136
+ error = ('TplinkRouterSG - Failed to get password keys: {}'.format(e))
137
+ if self._logger:
138
+ self._logger.debug(error)
139
+ raise ClientException(error)
140
+
141
+ def _request_auth_keys(self) -> None:
142
+ """Get sequence number and RSA public key for data encryption."""
143
+ url = '{}/cgi-bin/luci/;stok=/login?form=auth'.format(self.host)
144
+ response = post(
145
+ url, params={'operation': 'read'},
146
+ timeout=self.timeout, verify=self._verify_ssl,
147
+ )
148
+ try:
149
+ data = response.json()
150
+ self._seq = data['data']['seq']
151
+ self._nn = data['data']['key'][0]
152
+ self._ee = data['data']['key'][1]
153
+ except Exception as e:
154
+ error = ('TplinkRouterSG - Failed to get auth keys: {}'.format(e))
155
+ if self._logger:
156
+ self._logger.debug(error)
157
+ raise ClientException(error)
158
+
159
+ def _build_login_signature(self, data_len: int) -> str:
160
+ """Build RSA-OAEP encrypted signature for login requests."""
161
+ sign_str = '{}&h={}&s={}'.format(
162
+ self._get_aes_formatted_key(), self._hash, self._seq + data_len)
163
+ sign = ''
164
+ for i in range(0, len(sign_str), SIGNATURE_OFFSET):
165
+ chunk = sign_str[i:i + SIGNATURE_OFFSET]
166
+ sign += self._rsa_oaep_encrypt(chunk, self._nn, self._ee)
167
+ return sign
168
+
169
+ def _build_request_signature(self, data_len: int) -> str:
170
+ """Build HMAC-SHA256 signature for non-login requests."""
171
+ sign_str = 'h={}&s={}'.format(self._hash, self._seq + data_len)
172
+ aes_key = self._get_aes_formatted_key()
173
+ sign = ''
174
+ for i in range(0, len(sign_str), SIGNATURE_OFFSET):
175
+ chunk = sign_str[i:i + SIGNATURE_OFFSET]
176
+ h = hmac.new(aes_key.encode(), chunk.encode(), sha256)
177
+ sign += h.hexdigest()
178
+ return sign
179
+
180
+ def authorize(self) -> None:
181
+ """Authorize using SHA256 + OAEP encryption scheme."""
182
+ self._request_pwd_keys()
183
+ self._request_auth_keys()
184
+
185
+ # SHA256 hash of username + password (not MD5)
186
+ self._hash = sha256((self.username + self.password).encode()).hexdigest()
187
+
188
+ # Generate AES session key
189
+ self._generate_aes_key()
190
+
191
+ # RSA encrypt password with PKCS1 v1.5 (password uses v1.5, not OAEP)
192
+ encrypted_pwd = self._rsa_v15_encrypt(self.password, self._pwdNN, self._pwdEE)
193
+
194
+ # Build and AES-encrypt the login payload
195
+ login_data = 'operation=login&password={}&confirm=true'.format(encrypted_pwd)
196
+ encrypted_data = self._aes_encrypt(login_data)
197
+
198
+ # Build OAEP-encrypted signature
199
+ sign = self._build_login_signature(len(encrypted_data))
200
+
201
+ # Send login request
202
+ url = '{}/cgi-bin/luci/;stok=/login?form=login'.format(self.host)
203
+ body = 'sign={}&data={}'.format(sign, quote(encrypted_data))
204
+ response = post(
205
+ url, data=body, headers=self._headers_login,
206
+ timeout=self.timeout, verify=self._verify_ssl,
207
+ )
208
+
209
+ try:
210
+ resp = response.json()
211
+ decrypted = json.loads(self._aes_decrypt(resp['data']))
212
+
213
+ if not decrypted.get('success'):
214
+ error_data = decrypted.get('data', {})
215
+ raise ClientException(
216
+ 'TplinkRouterSG - Login failed: {}'.format(
217
+ error_data.get('errorcode', 'unknown')))
218
+
219
+ self._stok = decrypted['data']['stok']
220
+ if 'set-cookie' in response.headers:
221
+ regex_result = search(
222
+ r'sysauth=([^;]+)', response.headers['set-cookie'])
223
+ if regex_result:
224
+ self._sysauth = regex_result.group(1)
225
+ self._logged = True
226
+
227
+ except ClientException:
228
+ raise
229
+ except Exception as e:
230
+ error = ('TplinkRouterSG - Cannot authorize! Error - {}; Response - {}'
231
+ .format(e, response.text[:200] if response.text else ''))
232
+ if self._logger:
233
+ self._logger.debug(error)
234
+ raise ClientException(error)
235
+
236
+ def request(self, path: str, data: str,
237
+ ignore_response: bool = False,
238
+ ignore_errors: bool = False) -> dict | None:
239
+ """Make an authenticated request using HMAC-SHA256 signatures."""
240
+ if self._logged is False:
241
+ raise Exception('Not authorised')
242
+
243
+ # AES encrypt the request data
244
+ encrypted_data = self._aes_encrypt(data)
245
+
246
+ # REPLACE_HASH: update hash with SHA256 of encrypted data
247
+ self._hash = sha256(encrypted_data.encode()).hexdigest()
248
+
249
+ # Build HMAC-SHA256 signature
250
+ sign = self._build_request_signature(len(encrypted_data))
251
+
252
+ url = '{}/cgi-bin/luci/;stok={}/{}'.format(self.host, self._stok, path)
253
+ body = 'sign={}&data={}'.format(sign, quote(encrypted_data))
254
+ response = post(
255
+ url, data=body, headers=self._headers_request,
256
+ cookies={'sysauth': self._sysauth},
257
+ timeout=self.timeout, verify=self._verify_ssl,
258
+ )
259
+
260
+ if ignore_response:
261
+ return None
262
+
263
+ try:
264
+ resp = response.json()
265
+ decrypted = json.loads(self._aes_decrypt(resp['data']))
266
+
267
+ if self._is_valid_response(decrypted):
268
+ return decrypted.get(self._data_block)
269
+ elif ignore_errors:
270
+ return decrypted
271
+ except Exception as e:
272
+ error = ('TplinkRouterSG - Unknown response - {}; Request {} - Response {}'
273
+ .format(e, path, response.text[:200] if response.text else ''))
274
+ if self._logger:
275
+ self._logger.debug(error)
276
+ raise ClientError(error)
277
+
278
+ error = ('TplinkRouterSG - Response with error; Request {} - Response {}'
279
+ .format(path, response.text[:200] if response.text else ''))
280
+ if self._logger:
281
+ self._logger.debug(error)
282
+ raise ClientError(error)
283
+
284
+ def _is_valid_response(self, data: dict) -> bool:
285
+ return 'success' in data and data['success'] and self._data_block in data
@@ -22,6 +22,10 @@ class Device:
22
22
  packets_received: int | None = None
23
23
  down_speed: int | None = None
24
24
  up_speed: int | None = None
25
+ tx_rate: int | None = None
26
+ rx_rate: int | None = None
27
+ online_time: float | None = None
28
+ traffic_usage: int | None = None
25
29
  signal: int | None = None
26
30
  active: bool = True
27
31
 
@@ -3,6 +3,7 @@ from logging import Logger
3
3
  from tplinkrouterc6u import TPLinkXDRClient
4
4
  from tplinkrouterc6u.common.exception import ClientException
5
5
  from tplinkrouterc6u.client.c6u import TplinkRouter, TplinkRouterV1_11
6
+ from tplinkrouterc6u.client.sg import TplinkRouterSG
6
7
  from tplinkrouterc6u.client.deco import TPLinkDecoClient
7
8
  from tplinkrouterc6u.client_abstract import AbstractRouter
8
9
  from tplinkrouterc6u.client.mr import TPLinkMRClient, TPLinkMRClientGCM
@@ -37,6 +38,7 @@ class TplinkRouterProvider:
37
38
  TPLinkDecoClient,
38
39
  TPLinkXDRClient,
39
40
  TPLinkRClient,
41
+ TplinkRouterSG,
40
42
  TplinkRouterV1_11,
41
43
  TplinkRouter,
42
44
  TplinkC80Router,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tplinkrouterc6u
3
- Version: 5.14.0
3
+ Version: 5.15.0
4
4
  Summary: TP-Link Router API (supports also Mercusys Router)
5
5
  Home-page: https://github.com/AlexandrErohin/TP-Link-Archer-C6U
6
6
  Author: Alex Erohin
@@ -57,6 +57,7 @@ Python package for API access and management for TP-Link and Mercusys Routers. S
57
57
  from tplinkrouterc6u import (
58
58
  TplinkRouterProvider,
59
59
  TplinkRouterV1_11,
60
+ TplinkRouterSG, # For routers like Archer BE3600, Archer BE230
60
61
  TplinkRouter,
61
62
  TplinkC1200Router,
62
63
  TplinkC5400XRouter,
@@ -68,7 +69,7 @@ from tplinkrouterc6u import (
68
69
  TPLinkVR400v2Client,
69
70
  TPLinkEXClient, # Class for EX series routers which supports old firmwares with AES cipher CBC mode
70
71
  TPLinkEXClientGCM, # Class for EX series routers which supports AES cipher GCM mode
71
- TPLinkRClient,
72
+ TPLinkRClient, # For routers like TL-R470GP-AC
72
73
  TPLinkXDRClient,
73
74
  TPLinkDecoClient,
74
75
  TplinkC80Router,
@@ -83,11 +84,11 @@ router = TplinkRouterProvider.get_client('http://192.168.0.1', 'password')
83
84
  # You may use client directly like
84
85
  # router = TplinkRouter('http://192.168.0.1', 'password')
85
86
  # You may also pass username if it is different and a logger to log errors as
86
- # router = TplinkRouter('http://192.168.0.1','password','admin2', Logger('test'))
87
+ # router = TplinkRouter('http://192.168.0.1','password','admin2', logger=Logger('test'))
87
88
  # If you have the TP-link C5400X or similar, you can use the TplinkC5400XRouter class instead of the TplinkRouter class.
88
89
  # Remember that the password for this router is different, here you need to use the web encrypted password.
89
90
  # To get web encrypted password, read Web Encrypted Password section
90
- # router = TplinkC5400XRouter('http://192.168.0.1','WebEncryptedPassword', logger: Logger('test'))
91
+ # router = TplinkC5400XRouter('http://192.168.0.1','WebEncryptedPassword', logger=Logger('test'))
91
92
 
92
93
  try:
93
94
  router.authorize() # authorizing
@@ -202,6 +203,10 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
202
203
  | packets_received | total packets received | int, None |
203
204
  | down_speed | download speed | int, None |
204
205
  | up_speed | upload speed | int, None |
206
+ | tx_rate | transmit rate (Mbps) | int, None |
207
+ | rx_rate | receive rate (Mbps) | int, None |
208
+ | online_time | client online time (seconds) | float, None |
209
+ | traffic_usage | total traffic usage (bytes) | int, None |
205
210
  | signal | Signal strength | int, None |
206
211
  | active | Is active device | bool |
207
212
 
@@ -315,6 +320,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
315
320
  - Archer A7 V5
316
321
  - Archer A8 (1.0, 2.20)
317
322
  - Archer A9 V6
323
+ - Archer A10 v1
318
324
  - Archer A20 v1.0
319
325
  - Archer AX10 v1.0
320
326
  - Archer AX12 v1.0
@@ -344,10 +350,10 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
344
350
  - Archer BE550 v1.0
345
351
  - Archer BE800 v1.0
346
352
  - Archer BE805 v1.0
347
- - Archer BE3600 1.6
353
+ - Archer BE3600 (v1.0, v1.6)
348
354
  - Archer C1200 (v1.0, v2.0)
349
355
  - Archer C2300 (v1.0, v2.0)
350
- - Archer C3200
356
+ - Archer C3200 v1
351
357
  - Archer C6 (v2.0, v3.0, v3.20, 4.0)
352
358
  - Archer C6U v1.0
353
359
  - Archer C7 (v4.0, v5.0)
@@ -360,7 +366,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
360
366
  - Archer MR200 (v2, v5, v5.3, v6.0)
361
367
  - Archer MR550 v1
362
368
  - Archer MR600 (v1, v2, v3)
363
- - Archer NX200 v2.0
369
+ - Archer NX200 (v1.0, v2.0)
364
370
  - Archer VR400 (v2, v3)
365
371
  - Archer VR600 v3
366
372
  - Archer VR900v
@@ -387,6 +393,7 @@ or you have TP-link C5400X or similar router you need to get web encrypted passw
387
393
  - HX510 v1.0
388
394
  - M8550 v1
389
395
  - NE200-Outdoor v1.0
396
+ - NE211-Outdoor v1.0
390
397
  - NX510v v1.0
391
398
  - NX600 v2.0
392
399
  - TD-W9960 (v1, V1.20)
@@ -1,46 +1,48 @@
1
1
  test/__init__.py,sha256=McQmUjeN3AwmwdS6QNfwGXXE77OKoPK852I2BM9XsrU,210
2
2
  test/test_client_C3200.py,sha256=jQoGq3yasoOc2Da37RkZ8uT0uyFrKfX2OGGy7hq02A0,20932
3
3
  test/test_client_c1200.py,sha256=Sl-85JGqINNg-ckBZCIVqY0CC-V1UOc-yiIUljtePRM,7582
4
- test/test_client_c6u.py,sha256=GGYReY9RmnkJDgcaWtzbti-hTkab2D8JXxZCPt0wTfk,41662
4
+ test/test_client_c6u.py,sha256=ZdIMNpnLrwH6a4E6Myb8tz6S0qtiVOJ9ye4mmn-vVCI,46579
5
5
  test/test_client_c6u_v1_11.py,sha256=FkpDRYk-LKC_YFNyrDUZUm7_x89REMnl4zquiEtyFB4,3343
6
6
  test/test_client_c80.py,sha256=RY_1SgRVcQQdN9h0_IXA0YW4_0flEB_uel05QvDDfws,42359
7
7
  test/test_client_deco.py,sha256=YPLKRD8GoyDYHfRgdXvCk8iVNw8zdMJW-AHVnNbpdTM,31719
8
- test/test_client_ex.py,sha256=_HvQhT2dG63fg6kJkgnZoPwa9qCgFDjWSAYQfjgVRA4,40480
8
+ test/test_client_ex.py,sha256=IPPqFllDFCn2-StsV3cv3GuIDgwXM75x4wk8dSXN8eA,45556
9
9
  test/test_client_mr.py,sha256=HYn39wynxeWKyWLGKpnBrutnb1Yq6k1tp6_vH-L_UKQ,33839
10
10
  test/test_client_mr_200.py,sha256=86yANn5SUhVW6Uc5q5s_aTNL7tDnREeXk378G61v_TM,1186
11
11
  test/test_client_r.py,sha256=AMZklBTLDmnlluNu8hyJfty-1lnN9YReIT522D2Bf9c,20565
12
12
  test/test_client_re330.py,sha256=MgefuvOzfZtZOujrcOsjiTDiGEAujfeFXshcq7gn32Q,17044
13
+ test/test_client_sg.py,sha256=p8YlashUz370XFEY40PqiXE7CauEbUHEroTUjRIzjS8,7722
13
14
  test/test_client_vr400v2.py,sha256=J1MFUQKGX0czhYS2s8q1Fa8-aKAZ9RfWb0rE_yAxXmg,1813
14
15
  test/test_client_wdr.py,sha256=0ZnRNP57MbuMv2cxFS8iIoVyv8Q6gtY0Q03gtHp9AWY,13492
15
16
  test/test_client_xdr.py,sha256=o0d1mq5ev1wWcs7FvwYGMUKDSVZJREETZk__7Al5EtI,23640
16
- tplinkrouterc6u/__init__.py,sha256=K5c8sBDldVKcD_OfMB83U3xfEkLyRfxdE3VAtbsMMAo,1419
17
+ tplinkrouterc6u/__init__.py,sha256=ekYOi1CJEH-OvqC7OgsYPUTkwt0kokD3xGVhSLMUh6A,1472
17
18
  tplinkrouterc6u/client_abstract.py,sha256=3UYzmll774S_Gb5E0FTVO_rI3-XFM7PSklg1-V-2jls,1419
18
- tplinkrouterc6u/provider.py,sha256=XeUXzrpgo35DhFG7FvgcAtD0oZZlXl8OOyPfNAVfxMI,3224
19
+ tplinkrouterc6u/provider.py,sha256=TsKKhMSrE6ZhTJL3qf8T34lw3-t68KmHdp6EvzFaUwc,3316
19
20
  tplinkrouterc6u/client/__init__.py,sha256=KBy3fmtA9wgyFrb0Urh2x4CkKtWVnESdp-vxmuOvq0k,27
20
21
  tplinkrouterc6u/client/c1200.py,sha256=4XEYidEGmVIJk0YQLvmTnd0Gqa7glH2gUWvjreHpWrk,3178
21
22
  tplinkrouterc6u/client/c3200.py,sha256=GjaOjcVoKlRiSjn19d1rEIfZbIP_6SWsqDlsd_BbC7I,7057
22
23
  tplinkrouterc6u/client/c5400x.py,sha256=ID9jC-kLUBBeETvOh8cxyQpKmJBIzdwNYR03DmvMN0s,4289
23
- tplinkrouterc6u/client/c6u.py,sha256=7OGRnAfc9S7-v2-CrM0rayqesYkhIPouK7LJUV2opno,23574
24
+ tplinkrouterc6u/client/c6u.py,sha256=Owf-nZmqVK4lvP86_oYbs5kBdJtlfew_r8iHrasSv1Q,24130
24
25
  tplinkrouterc6u/client/c80.py,sha256=efE0DEjEfzRFr35fjKA_hsv9YaWy_2dgLAaurDM-WQk,17665
25
26
  tplinkrouterc6u/client/deco.py,sha256=cpKRggKD2RvSmMZuD6tzsZmehAUCU9oLiTTHcZBW81Y,8898
26
- tplinkrouterc6u/client/ex.py,sha256=qqlVJi2BIlMLjL9A2_Yi-5AFk18A1Ts8zW6qRPQQWQM,15016
27
+ tplinkrouterc6u/client/ex.py,sha256=ujUzhrRyzbV2vTIbDqiiuWybOOWVNjHHD8VnkGqbVjA,16556
27
28
  tplinkrouterc6u/client/mr.py,sha256=Yh7U0nWa_l5HKrUM5fDGewQlxHlyK-JcoPFXmSDgog4,29606
28
- tplinkrouterc6u/client/mr200.py,sha256=Z02ew7ZLCEdNyGB7mcPHPP-oaAUwJyt7euc2ubiuEo4,5891
29
+ tplinkrouterc6u/client/mr200.py,sha256=vqBnx8JzdegXzOBfyPo_AjsPpbYZHxzNSeW0cqBmTqY,5941
29
30
  tplinkrouterc6u/client/mr6400v7.py,sha256=PR7dk4Q0OMqMwnuExpHFjanCYAcZ2gjnJ0XoMV-ex8I,2407
30
31
  tplinkrouterc6u/client/r.py,sha256=H-qArD60gasT07pEeY48n0_c6-yUbAKL7IRmQtr6jXk,7632
31
32
  tplinkrouterc6u/client/re330.py,sha256=9Wj4VpYJbVwZJUh9s3magdeL3Jl-B7qyrWfrVBxRk4A,17465
33
+ tplinkrouterc6u/client/sg.py,sha256=WPxqxST84Dh-iO-V0oPRpehn2D21YwAq4r_4mEAFSv8,11353
32
34
  tplinkrouterc6u/client/vr.py,sha256=7Tbu0IrWtr4HHtyrnLFXEJi1QctzhilciL7agtwQ0R8,5025
33
35
  tplinkrouterc6u/client/vr400v2.py,sha256=ZgQ3w4s9cqhAYgq-xr7l64v1pCT3izdw6amG9XfM0cA,4208
34
36
  tplinkrouterc6u/client/wdr.py,sha256=i54PEifjhfOScDpgNBXygw9U4bfsVtle846_YjnDoBs,21679
35
37
  tplinkrouterc6u/client/xdr.py,sha256=CQN5h2f2oUuwjbtUeOjisBT8K1a72snIDxsWMc_qGik,10917
36
38
  tplinkrouterc6u/common/__init__.py,sha256=pCTvVZ9CAwgb7MxRnLx0y1rI0sTKSwT24FfxWfQXeTM,33
37
- tplinkrouterc6u/common/dataclass.py,sha256=NmwN6Iqpd9Ne7Zr-R0J1OZQz28NRp5Qzh6NjVFZV_DA,7749
39
+ tplinkrouterc6u/common/dataclass.py,sha256=9l_OepmU2JU8SJxYVOPvO_9PMu5nofKEqeFTSHB-s2c,7885
38
40
  tplinkrouterc6u/common/encryption.py,sha256=EWfgGafOz0YgPilBndVaupnjw6JrzhVBdZkBy3oWhj0,10229
39
41
  tplinkrouterc6u/common/exception.py,sha256=_0G8ZvW5__CsGifHrsZeULdl8c6EUD071sDCQsQgrHY,140
40
42
  tplinkrouterc6u/common/helper.py,sha256=23b04fk9HuVinrZXMCS5R1rmF8uZ7eM-Cdnp7Br9NR0,572
41
43
  tplinkrouterc6u/common/package_enum.py,sha256=CMHVSgk4RSZyFoPi3499-sJDYg-nfnyJbz1iArFU9Hw,1644
42
- tplinkrouterc6u-5.14.0.dist-info/licenses/LICENSE,sha256=YF6QR6Vjxcg5b_sYIyqkME7FZYau5TfEUGTG-0JeRK0,35129
43
- tplinkrouterc6u-5.14.0.dist-info/METADATA,sha256=9FKClNuCn3ur3yEcnUmUZ5G7eBBN-j1aCyfBQZGtxmY,17853
44
- tplinkrouterc6u-5.14.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
45
- tplinkrouterc6u-5.14.0.dist-info/top_level.txt,sha256=1iSCCIueqgEkrTxtQ-jiHe99jAB10zqrVdBcwvNfe_M,21
46
- tplinkrouterc6u-5.14.0.dist-info/RECORD,,
44
+ tplinkrouterc6u-5.15.0.dist-info/licenses/LICENSE,sha256=YF6QR6Vjxcg5b_sYIyqkME7FZYau5TfEUGTG-0JeRK0,35129
45
+ tplinkrouterc6u-5.15.0.dist-info/METADATA,sha256=9EEpcuq5euG0Beh4eGSldAafxtnnXob-bNX9bPnNEfY,18229
46
+ tplinkrouterc6u-5.15.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
47
+ tplinkrouterc6u-5.15.0.dist-info/top_level.txt,sha256=1iSCCIueqgEkrTxtQ-jiHe99jAB10zqrVdBcwvNfe_M,21
48
+ tplinkrouterc6u-5.15.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5