eeroctl 1.7.1__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.
Files changed (45) hide show
  1. eeroctl/__init__.py +19 -0
  2. eeroctl/commands/__init__.py +32 -0
  3. eeroctl/commands/activity.py +237 -0
  4. eeroctl/commands/auth.py +471 -0
  5. eeroctl/commands/completion.py +142 -0
  6. eeroctl/commands/device.py +492 -0
  7. eeroctl/commands/eero/__init__.py +12 -0
  8. eeroctl/commands/eero/base.py +224 -0
  9. eeroctl/commands/eero/led.py +154 -0
  10. eeroctl/commands/eero/nightlight.py +235 -0
  11. eeroctl/commands/eero/updates.py +82 -0
  12. eeroctl/commands/network/__init__.py +18 -0
  13. eeroctl/commands/network/advanced.py +191 -0
  14. eeroctl/commands/network/backup.py +162 -0
  15. eeroctl/commands/network/base.py +331 -0
  16. eeroctl/commands/network/dhcp.py +118 -0
  17. eeroctl/commands/network/dns.py +197 -0
  18. eeroctl/commands/network/forwards.py +115 -0
  19. eeroctl/commands/network/guest.py +162 -0
  20. eeroctl/commands/network/security.py +162 -0
  21. eeroctl/commands/network/speedtest.py +99 -0
  22. eeroctl/commands/network/sqm.py +194 -0
  23. eeroctl/commands/profile.py +671 -0
  24. eeroctl/commands/troubleshoot.py +317 -0
  25. eeroctl/context.py +254 -0
  26. eeroctl/errors.py +156 -0
  27. eeroctl/exit_codes.py +68 -0
  28. eeroctl/formatting/__init__.py +90 -0
  29. eeroctl/formatting/base.py +181 -0
  30. eeroctl/formatting/device.py +430 -0
  31. eeroctl/formatting/eero.py +591 -0
  32. eeroctl/formatting/misc.py +87 -0
  33. eeroctl/formatting/network.py +659 -0
  34. eeroctl/formatting/profile.py +443 -0
  35. eeroctl/main.py +161 -0
  36. eeroctl/options.py +429 -0
  37. eeroctl/output.py +739 -0
  38. eeroctl/safety.py +259 -0
  39. eeroctl/utils.py +181 -0
  40. eeroctl-1.7.1.dist-info/METADATA +115 -0
  41. eeroctl-1.7.1.dist-info/RECORD +45 -0
  42. eeroctl-1.7.1.dist-info/WHEEL +5 -0
  43. eeroctl-1.7.1.dist-info/entry_points.txt +3 -0
  44. eeroctl-1.7.1.dist-info/licenses/LICENSE +21 -0
  45. eeroctl-1.7.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,430 @@
1
+ """Device formatting utilities for the Eero CLI.
2
+
3
+ This module provides formatting functions for displaying connected device data.
4
+ """
5
+
6
+ from typing import List, Optional
7
+
8
+ from eero.models.device import Device
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+
12
+ from .base import (
13
+ DetailLevel,
14
+ build_panel,
15
+ console,
16
+ field,
17
+ field_bool,
18
+ field_status,
19
+ format_datetime,
20
+ format_device_status,
21
+ )
22
+
23
+ # ==================== Device Table ====================
24
+
25
+
26
+ def create_devices_table(devices: List[Device]) -> Table:
27
+ """Create a table displaying network devices.
28
+
29
+ Args:
30
+ devices: List of Device objects
31
+
32
+ Returns:
33
+ Rich Table object
34
+ """
35
+ table = Table(title="Connected Devices")
36
+ table.add_column("ID", style="dim")
37
+ table.add_column("Name", style="cyan")
38
+ table.add_column("Nickname", style="blue")
39
+ table.add_column("IP", style="green")
40
+ table.add_column("MAC", style="yellow")
41
+ table.add_column("Status", style="magenta")
42
+ table.add_column("Type", style="cyan")
43
+ table.add_column("Manufacturer", style="green")
44
+ table.add_column("Connection Type", style="blue")
45
+ table.add_column("Eero Location", style="yellow")
46
+ table.add_column("Interface", style="cyan")
47
+
48
+ for device in devices:
49
+ status_text, status_style = format_device_status(device.status)
50
+ device_name = device.display_name or device.hostname or device.nickname or "Unknown"
51
+ ip_address = device.ip or device.ipv4 or "Unknown"
52
+ mac_address = device.mac or "Unknown"
53
+ connection_type = device.connection_type or "Unknown"
54
+ eero_location = device.source.location if device.source else "Unknown"
55
+
56
+ interface_info = ""
57
+ if device.interface:
58
+ if device.interface.frequency and device.interface.frequency_unit:
59
+ interface_info = f"{device.interface.frequency} {device.interface.frequency_unit}"
60
+ elif device.interface.frequency:
61
+ interface_info = f"{device.interface.frequency} GHz"
62
+ elif device.connectivity and device.connectivity.frequency:
63
+ interface_info = f"{device.connectivity.frequency} MHz"
64
+
65
+ table.add_row(
66
+ device.id or "Unknown",
67
+ device_name,
68
+ device.nickname or "",
69
+ ip_address,
70
+ mac_address,
71
+ f"[{status_style}]{status_text}[/{status_style}]",
72
+ device.device_type or "Unknown",
73
+ device.manufacturer or "Unknown",
74
+ connection_type,
75
+ eero_location,
76
+ interface_info or "Unknown",
77
+ )
78
+
79
+ return table
80
+
81
+
82
+ # ==================== Device Brief View Panels ====================
83
+
84
+
85
+ def _device_basic_panel(device: Device, extensive: bool = False) -> Panel:
86
+ """Build the basic device info panel."""
87
+ status_text, status_style = format_device_status(device.status)
88
+ device_name = device.display_name or device.hostname or device.nickname or "Unknown"
89
+ ip_address = device.ip or device.ipv4 or "Unknown"
90
+ mac_address = device.mac or "Unknown"
91
+
92
+ # Profile display
93
+ profile_display = "None"
94
+ if device.profile:
95
+ profile_name = device.profile.name or "Unknown"
96
+ profile_id = device.profile_id or "Unknown"
97
+ profile_display = f"{profile_name} ({profile_id})"
98
+ elif device.profile_id:
99
+ profile_display = f"Unknown ({device.profile_id})"
100
+
101
+ lines = [
102
+ field("Name", device_name),
103
+ field("Nickname", device.nickname, "None"),
104
+ field("MAC Address", mac_address),
105
+ field("IP Address", ip_address),
106
+ field("Hostname", device.hostname),
107
+ field_status("Status", status_text, status_style),
108
+ field("Manufacturer", device.manufacturer),
109
+ field("Model", device.model_name),
110
+ field("Type", device.device_type),
111
+ field_bool("Connected", device.connected),
112
+ field_bool("Guest", device.is_guest),
113
+ field_bool("Paused", device.paused),
114
+ field_bool("Blocked", device.blacklisted),
115
+ field("Profile", profile_display),
116
+ field("Connection Type", device.connection_type),
117
+ ]
118
+
119
+ if extensive:
120
+ lines.append(field("Eero Location", device.source.location if device.source else "Unknown"))
121
+
122
+ return build_panel(lines, f"Device: {device_name}", "blue")
123
+
124
+
125
+ def _device_connected_eero_panel(device: Device) -> Optional[Panel]:
126
+ """Build the connected eero panel for brief view."""
127
+ source = device.source
128
+ if not source:
129
+ return None
130
+
131
+ location = getattr(source, "location", None)
132
+ model = getattr(source, "model", None)
133
+ display_name = getattr(source, "display_name", None)
134
+ is_gateway = getattr(source, "is_gateway", False)
135
+
136
+ lines = []
137
+
138
+ if location:
139
+ lines.append(field("Location", location))
140
+ if display_name and display_name != location:
141
+ lines.append(field("Eero Name", display_name))
142
+ if model:
143
+ lines.append(field("Model", model))
144
+ if is_gateway:
145
+ lines.append("[bold]Role:[/bold] [cyan]Gateway[/cyan]")
146
+
147
+ return build_panel(lines, "Connected Eero", "green") if lines else None
148
+
149
+
150
+ def _device_connectivity_panel(device: Device) -> Optional[Panel]:
151
+ """Build the connectivity panel for brief view."""
152
+ connectivity = device.connectivity
153
+ if not connectivity:
154
+ return None
155
+
156
+ lines = []
157
+
158
+ # Signal strength with visual indicator
159
+ signal = getattr(connectivity, "signal", None)
160
+ if signal:
161
+ try:
162
+ signal_val = int(str(signal).replace(" dBm", "").replace("dBm", ""))
163
+ if signal_val >= -50:
164
+ signal_style = "green"
165
+ elif signal_val >= -70:
166
+ signal_style = "yellow"
167
+ else:
168
+ signal_style = "red"
169
+ lines.append(f"[bold]Signal:[/bold] [{signal_style}]{signal}[/{signal_style}]")
170
+ except (ValueError, AttributeError):
171
+ lines.append(field("Signal", signal))
172
+
173
+ # Score bars with visual indicator
174
+ score_bars = getattr(connectivity, "score_bars", None)
175
+ if score_bars is not None:
176
+ bars_style = "green" if score_bars >= 4 else "yellow" if score_bars >= 2 else "red"
177
+ filled = "●" * score_bars
178
+ empty = "○" * (5 - score_bars)
179
+ lines.append(
180
+ f"[bold]Quality:[/bold] [{bars_style}]{filled}{empty} ({score_bars}/5)[/{bars_style}]"
181
+ )
182
+
183
+ # Frequency
184
+ frequency = getattr(connectivity, "frequency", None)
185
+ if frequency:
186
+ band = "5 GHz" if frequency > 3000 else "2.4 GHz"
187
+ lines.append(field("Frequency", f"{frequency} MHz ({band})"))
188
+
189
+ # Bitrates
190
+ rx_bitrate = getattr(connectivity, "rx_bitrate", None)
191
+ if rx_bitrate:
192
+ lines.append(field("RX Bitrate", rx_bitrate))
193
+
194
+ tx_bitrate = getattr(connectivity, "tx_bitrate", None)
195
+ if tx_bitrate:
196
+ lines.append(field("TX Bitrate", tx_bitrate))
197
+
198
+ # Channel width from rx_rate_info
199
+ rx_rate_info = getattr(connectivity, "rx_rate_info", None)
200
+ if rx_rate_info:
201
+ channel_width = rx_rate_info.get("channel_width", "")
202
+ if channel_width:
203
+ width_display = channel_width.replace("WIDTH_", "").replace("MHz", " MHz")
204
+ lines.append(field("Channel Width", width_display))
205
+
206
+ phy_type = rx_rate_info.get("phy_type", "")
207
+ if phy_type:
208
+ phy_map = {
209
+ "HE": "WiFi 6 (802.11ax)",
210
+ "VHT": "WiFi 5 (802.11ac)",
211
+ "HT": "WiFi 4 (802.11n)",
212
+ }
213
+ lines.append(field("PHY Type", phy_map.get(phy_type, phy_type)))
214
+
215
+ return build_panel(lines, "Connectivity", "cyan") if lines else None
216
+
217
+
218
+ def _device_wifi_panel(device: Device) -> Optional[Panel]:
219
+ """Build the WiFi details panel for brief view."""
220
+ wireless = getattr(device, "wireless", False)
221
+ connection_type = device.connection_type
222
+ if not wireless and connection_type != "wireless":
223
+ return None
224
+
225
+ lines = []
226
+
227
+ ssid = getattr(device, "ssid", None)
228
+ if ssid:
229
+ lines.append(field("Network (SSID)", ssid))
230
+
231
+ interface = device.interface
232
+ if interface:
233
+ freq = getattr(interface, "frequency", None)
234
+ freq_unit = getattr(interface, "frequency_unit", None)
235
+ if freq:
236
+ freq_display = f"{freq} {freq_unit}" if freq_unit else f"{freq} GHz"
237
+ lines.append(field("Band", freq_display))
238
+
239
+ channel = getattr(device, "channel", None)
240
+ if channel:
241
+ lines.append(field("Channel", channel))
242
+
243
+ auth = getattr(device, "auth", None)
244
+ if auth:
245
+ auth_display = auth.upper() if auth else "Unknown"
246
+ lines.append(field("Security", auth_display))
247
+
248
+ subnet_kind = getattr(device, "subnet_kind", None)
249
+ if subnet_kind:
250
+ lines.append(field("Subnet", subnet_kind))
251
+
252
+ return build_panel(lines, "WiFi Details", "magenta") if lines else None
253
+
254
+
255
+ def _device_timing_panel(device: Device) -> Optional[Panel]:
256
+ """Build the timing panel for brief view."""
257
+ last_active = getattr(device, "last_active", None)
258
+ first_active = getattr(device, "first_active", None)
259
+
260
+ if not (last_active or first_active):
261
+ return None
262
+
263
+ lines = []
264
+
265
+ if last_active:
266
+ lines.append(field("Last Active", format_datetime(last_active)))
267
+
268
+ if first_active:
269
+ lines.append(field("First Seen", format_datetime(first_active)))
270
+
271
+ return build_panel(lines, "Activity", "yellow")
272
+
273
+
274
+ def _device_ips_panel(device: Device) -> Optional[Panel]:
275
+ """Build the IP addresses panel for brief view."""
276
+ ips = getattr(device, "ips", [])
277
+
278
+ if not ips or len(ips) <= 1:
279
+ return None
280
+
281
+ lines = ["[bold]All IP Addresses:[/bold]"]
282
+ for ip in ips:
283
+ if ":" in str(ip):
284
+ lines.append(f" • {ip} [dim](IPv6)[/dim]")
285
+ else:
286
+ lines.append(f" • {ip}")
287
+
288
+ return build_panel(lines, "IP Addresses", "blue")
289
+
290
+
291
+ def _device_connection_panel(device: Device) -> Optional[Panel]:
292
+ """Build the connection details panel for brief view."""
293
+ connection = device.connection
294
+ if not connection:
295
+ return None
296
+
297
+ lines = [
298
+ field("Type", connection.type),
299
+ field("Connected To", connection.connected_to),
300
+ field("Connected Via", connection.connected_via),
301
+ ]
302
+
303
+ if connection.frequency:
304
+ lines.append(field("Frequency", connection.frequency))
305
+ if connection.signal_strength is not None:
306
+ lines.append(field("Signal Strength", connection.signal_strength))
307
+ if connection.tx_rate is not None:
308
+ lines.append(field("TX Rate", f"{connection.tx_rate} Mbps"))
309
+ if connection.rx_rate is not None:
310
+ lines.append(field("RX Rate", f"{connection.rx_rate} Mbps"))
311
+
312
+ return build_panel(lines, "Connection Details", "green")
313
+
314
+
315
+ def _device_tags_panel(device: Device) -> Optional[Panel]:
316
+ """Build the tags panel."""
317
+ tags = device.tags
318
+ if not tags:
319
+ return None
320
+
321
+ lines = [f"[bold]{tag.name}:[/bold] {tag.color or 'No color'}" for tag in tags]
322
+ return build_panel(lines, "Tags", "yellow")
323
+
324
+
325
+ # ==================== Device Extensive View Panels ====================
326
+
327
+
328
+ def _device_connectivity_extensive_panel(device: Device) -> Optional[Panel]:
329
+ """Build the connectivity panel for extensive view."""
330
+ connectivity = device.connectivity
331
+ if not connectivity:
332
+ return None
333
+
334
+ channel_width = "N/A"
335
+ if connectivity.rx_rate_info and "channel_width" in connectivity.rx_rate_info:
336
+ channel_width = connectivity.rx_rate_info["channel_width"]
337
+
338
+ lines = [
339
+ field("Signal", connectivity.signal, "N/A"),
340
+ field("Score", connectivity.score, "N/A"),
341
+ field("Score Bars", connectivity.score_bars, "N/A"),
342
+ field("Frequency", f"{connectivity.frequency or 'N/A'} MHz"),
343
+ field("RX Bitrate", connectivity.rx_bitrate, "N/A"),
344
+ field("Channel Width", channel_width),
345
+ ]
346
+ return build_panel(lines, "Connectivity", "green")
347
+
348
+
349
+ def _device_interface_panel(device: Device) -> Optional[Panel]:
350
+ """Build the interface panel for extensive view."""
351
+ interface = device.interface
352
+ if not interface:
353
+ return None
354
+
355
+ lines = [
356
+ field(
357
+ "Frequency",
358
+ f"{interface.frequency or 'N/A'} {interface.frequency_unit or ''}",
359
+ ),
360
+ field("Channel", device.channel, "N/A"),
361
+ field("Authentication", device.auth, "N/A"),
362
+ ]
363
+ return build_panel(lines, "Interface", "cyan")
364
+
365
+
366
+ # ==================== Main Device Details Function ====================
367
+
368
+
369
+ def print_device_details(device: Device, detail_level: DetailLevel = "brief") -> None:
370
+ """Print device information with configurable detail level.
371
+
372
+ Args:
373
+ device: Device object
374
+ detail_level: "brief" or "full"
375
+ """
376
+ extensive = detail_level == "full"
377
+
378
+ # Basic info panel (always shown)
379
+ console.print(_device_basic_panel(device, extensive))
380
+
381
+ if not extensive:
382
+ # Brief view panels
383
+
384
+ # Connected eero info
385
+ eero_panel = _device_connected_eero_panel(device)
386
+ if eero_panel:
387
+ console.print(eero_panel)
388
+
389
+ # Connectivity info
390
+ connectivity_panel = _device_connectivity_panel(device)
391
+ if connectivity_panel:
392
+ console.print(connectivity_panel)
393
+
394
+ # WiFi details
395
+ wifi_panel = _device_wifi_panel(device)
396
+ if wifi_panel:
397
+ console.print(wifi_panel)
398
+
399
+ # Activity/timing
400
+ timing_panel = _device_timing_panel(device)
401
+ if timing_panel:
402
+ console.print(timing_panel)
403
+
404
+ # IP addresses (if multiple)
405
+ ips_panel = _device_ips_panel(device)
406
+ if ips_panel:
407
+ console.print(ips_panel)
408
+
409
+ # Connection details (if available)
410
+ connection_panel = _device_connection_panel(device)
411
+ if connection_panel:
412
+ console.print(connection_panel)
413
+
414
+ # Tags
415
+ tags_panel = _device_tags_panel(device)
416
+ if tags_panel:
417
+ console.print(tags_panel)
418
+
419
+ else:
420
+ # Extensive view panels
421
+
422
+ # Connectivity
423
+ connectivity_panel = _device_connectivity_extensive_panel(device)
424
+ if connectivity_panel:
425
+ console.print(connectivity_panel)
426
+
427
+ # Interface
428
+ interface_panel = _device_interface_panel(device)
429
+ if interface_panel:
430
+ console.print(interface_panel)