router-cli 0.1.0__py3-none-any.whl → 0.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.
- router_cli/config.py +83 -7
- router_cli/main.py +21 -12
- {router_cli-0.1.0.dist-info → router_cli-0.2.0.dist-info}/METADATA +28 -4
- router_cli-0.2.0.dist-info/RECORD +9 -0
- router_cli-0.1.0.dist-info/RECORD +0 -9
- {router_cli-0.1.0.dist-info → router_cli-0.2.0.dist-info}/WHEEL +0 -0
- {router_cli-0.1.0.dist-info → router_cli-0.2.0.dist-info}/entry_points.txt +0 -0
router_cli/config.py
CHANGED
|
@@ -47,20 +47,96 @@ def load_config() -> dict:
|
|
|
47
47
|
'password = "your_password"\n'
|
|
48
48
|
"\n"
|
|
49
49
|
"[known_devices]\n"
|
|
50
|
-
'"AA:BB:CC:DD:EE:FF" = "My Phone"\n'
|
|
51
|
-
'"
|
|
50
|
+
'"AA:BB:CC:DD:EE:FF" = "My Phone" # by MAC address\n'
|
|
51
|
+
'"android-abc123" = "Pixel Phone" # by hostname (for random MACs)'
|
|
52
52
|
)
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def
|
|
55
|
+
def _is_mac_address(identifier: str) -> bool:
|
|
56
|
+
"""Check if a string looks like a MAC address.
|
|
57
|
+
|
|
58
|
+
MAC addresses contain colons and are in format XX:XX:XX:XX:XX:XX
|
|
59
|
+
"""
|
|
60
|
+
import re
|
|
61
|
+
|
|
62
|
+
# Match typical MAC address formats (with colons)
|
|
63
|
+
return bool(re.match(r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$", identifier))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class KnownDevices:
|
|
67
|
+
"""Container for known devices, supporting lookup by MAC or hostname.
|
|
68
|
+
|
|
69
|
+
This class supports devices that use random MAC addresses by allowing
|
|
70
|
+
hostname-based identification in addition to MAC-based.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
by_mac: dict[str, str] | None = None,
|
|
76
|
+
by_hostname: dict[str, str] | None = None,
|
|
77
|
+
):
|
|
78
|
+
self.by_mac: dict[str, str] = by_mac or {}
|
|
79
|
+
self.by_hostname: dict[str, str] = by_hostname or {}
|
|
80
|
+
|
|
81
|
+
def get_alias(self, mac: str, hostname: str = "") -> str | None:
|
|
82
|
+
"""Get alias for a device by MAC or hostname.
|
|
83
|
+
|
|
84
|
+
MAC lookup takes priority. Returns None if device is not known.
|
|
85
|
+
"""
|
|
86
|
+
# First try MAC lookup (normalized to uppercase)
|
|
87
|
+
alias = self.by_mac.get(mac.upper())
|
|
88
|
+
if alias:
|
|
89
|
+
return alias
|
|
90
|
+
|
|
91
|
+
# Then try hostname lookup (case-insensitive)
|
|
92
|
+
if hostname:
|
|
93
|
+
alias = self.by_hostname.get(hostname.lower())
|
|
94
|
+
if alias:
|
|
95
|
+
return alias
|
|
96
|
+
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def is_known(self, mac: str, hostname: str = "") -> bool:
|
|
100
|
+
"""Check if a device is known by MAC or hostname."""
|
|
101
|
+
return self.get_alias(mac, hostname) is not None
|
|
102
|
+
|
|
103
|
+
def get(self, key: str, default: str | None = None) -> str | None:
|
|
104
|
+
"""Dict-like get for backward compatibility (MAC lookup only)."""
|
|
105
|
+
return self.by_mac.get(key, default)
|
|
106
|
+
|
|
107
|
+
def __contains__(self, key: str) -> bool:
|
|
108
|
+
"""Dict-like contains for backward compatibility (MAC lookup only)."""
|
|
109
|
+
return key in self.by_mac
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def load_known_devices() -> KnownDevices:
|
|
56
113
|
"""Load known devices from config file.
|
|
57
114
|
|
|
58
|
-
Returns a
|
|
115
|
+
Returns a KnownDevices object supporting lookup by MAC address or hostname.
|
|
116
|
+
|
|
117
|
+
Config format:
|
|
118
|
+
[known_devices]
|
|
119
|
+
"AA:BB:CC:DD:EE:FF" = "My Phone" # MAC-based (for stable MACs)
|
|
120
|
+
"android-abc123" = "John's Pixel" # Hostname-based (for random MACs)
|
|
121
|
+
|
|
122
|
+
MAC addresses are identified by format (XX:XX:XX:XX:XX:XX with colons).
|
|
123
|
+
Any other identifier is treated as a hostname.
|
|
59
124
|
"""
|
|
60
125
|
config = _load_config_file()
|
|
61
126
|
if config is None:
|
|
62
|
-
return
|
|
127
|
+
return KnownDevices()
|
|
63
128
|
|
|
64
129
|
known = config.get("known_devices", {})
|
|
65
|
-
|
|
66
|
-
|
|
130
|
+
|
|
131
|
+
by_mac: dict[str, str] = {}
|
|
132
|
+
by_hostname: dict[str, str] = {}
|
|
133
|
+
|
|
134
|
+
for identifier, alias in known.items():
|
|
135
|
+
if _is_mac_address(identifier):
|
|
136
|
+
# MAC address - normalize to uppercase
|
|
137
|
+
by_mac[identifier.upper()] = alias
|
|
138
|
+
else:
|
|
139
|
+
# Hostname - normalize to lowercase for case-insensitive matching
|
|
140
|
+
by_hostname[identifier.lower()] = alias
|
|
141
|
+
|
|
142
|
+
return KnownDevices(by_mac=by_mac, by_hostname=by_hostname)
|
router_cli/main.py
CHANGED
|
@@ -22,7 +22,7 @@ from .client import (
|
|
|
22
22
|
Statistics,
|
|
23
23
|
WirelessClient,
|
|
24
24
|
)
|
|
25
|
-
from .config import load_config, load_known_devices
|
|
25
|
+
from .config import KnownDevices, load_config, load_known_devices
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
# ANSI color codes
|
|
@@ -130,14 +130,20 @@ def spinner(message: str = "Loading..."):
|
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
def get_device_display(
|
|
133
|
-
mac: str, hostname: str, known_devices:
|
|
133
|
+
mac: str, hostname: str, known_devices: KnownDevices | None
|
|
134
134
|
) -> tuple[str, bool]:
|
|
135
135
|
"""Get display name for a device and whether it's known.
|
|
136
136
|
|
|
137
137
|
Returns (display_name, is_known) tuple.
|
|
138
138
|
If known, display_name is 'Alias (hostname)'.
|
|
139
|
+
|
|
140
|
+
Supports lookup by both MAC address and hostname (for devices with
|
|
141
|
+
random MAC addresses like some Android phones).
|
|
139
142
|
"""
|
|
140
|
-
|
|
143
|
+
if known_devices is None:
|
|
144
|
+
return hostname or mac, False
|
|
145
|
+
|
|
146
|
+
alias = known_devices.get_alias(mac, hostname)
|
|
141
147
|
if alias:
|
|
142
148
|
if hostname and hostname != alias:
|
|
143
149
|
return f"{alias} ({hostname})", True
|
|
@@ -220,21 +226,22 @@ def format_status(status: RouterStatus) -> str:
|
|
|
220
226
|
|
|
221
227
|
|
|
222
228
|
def format_clients(
|
|
223
|
-
clients: list[WirelessClient], known_devices:
|
|
229
|
+
clients: list[WirelessClient], known_devices: KnownDevices | None = None
|
|
224
230
|
) -> str:
|
|
225
231
|
"""Format wireless clients for display."""
|
|
226
232
|
if not clients:
|
|
227
233
|
return "No wireless clients connected."
|
|
228
234
|
|
|
229
|
-
known_devices = known_devices or
|
|
235
|
+
known_devices = known_devices or KnownDevices()
|
|
230
236
|
|
|
231
237
|
# Build display data and calculate column widths
|
|
232
238
|
rows = []
|
|
233
239
|
for c in clients:
|
|
234
240
|
assoc = "Yes" if c.associated else "No"
|
|
235
241
|
auth = "Yes" if c.authorized else "No"
|
|
236
|
-
|
|
237
|
-
alias = known_devices.
|
|
242
|
+
# WirelessClient doesn't have hostname, so we can only check by MAC
|
|
243
|
+
alias = known_devices.get_alias(c.mac, "")
|
|
244
|
+
is_known = alias is not None
|
|
238
245
|
mac_display = f"{c.mac} ({alias})" if alias else c.mac
|
|
239
246
|
rows.append((mac_display, assoc, auth, c.ssid, c.interface, is_known))
|
|
240
247
|
|
|
@@ -268,13 +275,13 @@ def format_clients(
|
|
|
268
275
|
|
|
269
276
|
|
|
270
277
|
def format_dhcp(
|
|
271
|
-
leases: list[DHCPLease], known_devices:
|
|
278
|
+
leases: list[DHCPLease], known_devices: KnownDevices | None = None
|
|
272
279
|
) -> str:
|
|
273
280
|
"""Format DHCP leases for display."""
|
|
274
281
|
if not leases:
|
|
275
282
|
return "No DHCP leases."
|
|
276
283
|
|
|
277
|
-
known_devices = known_devices or
|
|
284
|
+
known_devices = known_devices or KnownDevices()
|
|
278
285
|
|
|
279
286
|
# Build display data and calculate column widths
|
|
280
287
|
rows = []
|
|
@@ -421,10 +428,10 @@ def format_overview(
|
|
|
421
428
|
clients: list[WirelessClient],
|
|
422
429
|
leases: list[DHCPLease],
|
|
423
430
|
stats: Statistics,
|
|
424
|
-
known_devices:
|
|
431
|
+
known_devices: KnownDevices | None = None,
|
|
425
432
|
) -> str:
|
|
426
433
|
"""Format overview dashboard with highlights from multiple sources."""
|
|
427
|
-
known_devices = known_devices or
|
|
434
|
+
known_devices = known_devices or KnownDevices()
|
|
428
435
|
lines = [
|
|
429
436
|
"=" * 60,
|
|
430
437
|
f"{'ROUTER OVERVIEW':^60}",
|
|
@@ -512,7 +519,9 @@ def format_overview(
|
|
|
512
519
|
f" Low SNR margin: {stats.adsl.downstream_snr_margin:.1f} dB (may cause disconnects)"
|
|
513
520
|
)
|
|
514
521
|
# Warn about unknown devices
|
|
515
|
-
unknown_count = sum(
|
|
522
|
+
unknown_count = sum(
|
|
523
|
+
1 for lease in leases if not known_devices.is_known(lease.mac, lease.hostname)
|
|
524
|
+
)
|
|
516
525
|
if unknown_count > 0:
|
|
517
526
|
warnings.append(
|
|
518
527
|
colorize(f" Unknown devices on network: {unknown_count}", "red")
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: router-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CLI tool to manage D-Link DSL-2750U router
|
|
5
|
+
Keywords: router,cli,dlink,dsl-2750u,network,admin
|
|
5
6
|
Author: d3vr
|
|
6
7
|
Author-email: d3vr <hi@f3.al>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: System :: Networking
|
|
7
19
|
Requires-Dist: tomli>=2.0.0 ; python_full_version < '3.11'
|
|
8
20
|
Requires-Python: >=3.10
|
|
21
|
+
Project-URL: Homepage, https://github.com/d3vr/router-cli
|
|
22
|
+
Project-URL: Repository, https://github.com/d3vr/router-cli
|
|
23
|
+
Project-URL: Issues, https://github.com/d3vr/router-cli/issues
|
|
9
24
|
Description-Content-Type: text/markdown
|
|
10
25
|
|
|
11
26
|
# router-cli
|
|
@@ -58,10 +73,12 @@ username = "admin"
|
|
|
58
73
|
password = "your_password_here"
|
|
59
74
|
|
|
60
75
|
# Optional: Define known devices for colorized output
|
|
76
|
+
# Use MAC addresses for devices with stable MACs
|
|
77
|
+
# Use hostnames for devices with random MACs (like Android)
|
|
61
78
|
[known_devices]
|
|
62
79
|
"AA:BB:CC:DD:EE:FF" = "My Phone"
|
|
63
80
|
"11:22:33:44:55:66" = "Smart TV"
|
|
64
|
-
"
|
|
81
|
+
"android-abc123" = "John's Pixel"
|
|
65
82
|
```
|
|
66
83
|
|
|
67
84
|
The tool searches for configuration in the following locations (in order):
|
|
@@ -189,7 +206,7 @@ Sends a reboot command to the router.
|
|
|
189
206
|
|
|
190
207
|
## Known Devices
|
|
191
208
|
|
|
192
|
-
The `[known_devices]` section in your config file maps MAC addresses to friendly names. This enables:
|
|
209
|
+
The `[known_devices]` section in your config file maps MAC addresses or hostnames to friendly names. This enables:
|
|
193
210
|
|
|
194
211
|
- **Color-coded output**: Known devices appear in green, unknown in red
|
|
195
212
|
- **Friendly names**: See "My Phone" instead of a hostname or MAC address
|
|
@@ -197,11 +214,18 @@ The `[known_devices]` section in your config file maps MAC addresses to friendly
|
|
|
197
214
|
|
|
198
215
|
```toml
|
|
199
216
|
[known_devices]
|
|
217
|
+
# By MAC address (for devices with stable MACs)
|
|
200
218
|
"AA:BB:CC:DD:EE:FF" = "My Phone"
|
|
201
219
|
"11:22:33:44:55:66" = "Smart TV"
|
|
220
|
+
|
|
221
|
+
# By hostname (for devices with random MACs, like some Android phones)
|
|
222
|
+
"android-abc123def" = "John's Pixel"
|
|
223
|
+
"Galaxy-S24" = "Sarah's Phone"
|
|
202
224
|
```
|
|
203
225
|
|
|
204
|
-
MAC addresses are matched case-insensitively.
|
|
226
|
+
**MAC addresses** are identified by their format (`XX:XX:XX:XX:XX:XX`) and matched case-insensitively.
|
|
227
|
+
|
|
228
|
+
**Hostnames** are useful for Android and iOS devices that use MAC address randomization for privacy. These devices typically maintain a consistent hostname even when their MAC changes. Hostnames are also matched case-insensitively.
|
|
205
229
|
|
|
206
230
|
## Development
|
|
207
231
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
router_cli/__init__.py,sha256=R-j6ZUnvZT4qKwxPGaYBQEOCLsPATLn1qHLHSmdfo2c,74
|
|
2
|
+
router_cli/client.py,sha256=DuOqZvsCkcgut2k0LqKcdYD_rN4E1TRtwfqm9bnJTEU,29631
|
|
3
|
+
router_cli/config.py,sha256=fn0_Mcy0Py-7k-yZwPEsXl3MhcspkKgzal-JHjBfAD8,4446
|
|
4
|
+
router_cli/main.py,sha256=iLCR1VaPrb1PvXpPdNciH7Y2-68zXAQ9YK0TmzepgJ8,22809
|
|
5
|
+
router_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
router_cli-0.2.0.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
7
|
+
router_cli-0.2.0.dist-info/entry_points.txt,sha256=PfkydZeDJvlgvEXYOG1gYvbTM39Cm-OgsPpREH3UNAU,49
|
|
8
|
+
router_cli-0.2.0.dist-info/METADATA,sha256=8zwKVYYwOP5F7QDRnS-xB-svOE3VTPe-EZM-8D4iO6A,6826
|
|
9
|
+
router_cli-0.2.0.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
router_cli/__init__.py,sha256=R-j6ZUnvZT4qKwxPGaYBQEOCLsPATLn1qHLHSmdfo2c,74
|
|
2
|
-
router_cli/client.py,sha256=DuOqZvsCkcgut2k0LqKcdYD_rN4E1TRtwfqm9bnJTEU,29631
|
|
3
|
-
router_cli/config.py,sha256=SX1s1CJk_XiiCHlZ47I9vgda9IRqqUwBTQKjC3BYoVE,1803
|
|
4
|
-
router_cli/main.py,sha256=UzIYNKIM9f4NQ2UW-wenI8yQRVKMVMiPyaXPjyTggaU,22469
|
|
5
|
-
router_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
router_cli-0.1.0.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
7
|
-
router_cli-0.1.0.dist-info/entry_points.txt,sha256=PfkydZeDJvlgvEXYOG1gYvbTM39Cm-OgsPpREH3UNAU,49
|
|
8
|
-
router_cli-0.1.0.dist-info/METADATA,sha256=HqMMKyryg1MFSZneA_OMyN3rR7mJ_7VBp7OCcjuKHJA,5509
|
|
9
|
-
router_cli-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|