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 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
- '"11:22:33:44:55:66" = "Smart TV"'
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 load_known_devices() -> dict[str, str]:
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 dict mapping MAC addresses (uppercase) to aliases.
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
- # Normalize MAC addresses to uppercase for case-insensitive matching
66
- return {mac.upper(): alias for mac, alias in known.items()}
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: dict[str, str]
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
- alias = known_devices.get(mac.upper())
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: dict[str, str] | None = None
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
- is_known = c.mac.upper() in known_devices
237
- alias = known_devices.get(c.mac.upper(), "")
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: dict[str, str] | None = None
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: dict[str, str] | None = None,
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(1 for lease in leases if lease.mac.upper() not in known_devices)
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.1.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
- "DE:AD:BE:EF:00:01" = "Work Laptop"
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,,