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,591 @@
1
+ """Eero device formatting utilities for the Eero CLI.
2
+
3
+ This module provides formatting functions for displaying Eero mesh node data.
4
+ """
5
+
6
+ from typing import Any, List, Optional
7
+
8
+ from eero.models.eero import Eero
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_eero_status,
21
+ )
22
+
23
+ # ==================== Helper Functions ====================
24
+
25
+
26
+ def _format_ethernet_speed(speed: Optional[str]) -> str:
27
+ """Format ethernet port speed for display."""
28
+ if not speed:
29
+ return "Unknown"
30
+ speed_map = {
31
+ "P10000": "10 Gbps",
32
+ "P2500": "2.5 Gbps",
33
+ "P1000": "1 Gbps",
34
+ "P100": "100 Mbps",
35
+ "P10": "10 Mbps",
36
+ }
37
+ return speed_map.get(speed, speed)
38
+
39
+
40
+ def _format_band(band: str) -> str:
41
+ """Format band string for display."""
42
+ band_map = {
43
+ "band_2_4GHz": "2.4 GHz",
44
+ "band_5GHz": "5 GHz",
45
+ "band_5GHz_full": "5 GHz",
46
+ "band_5GHz_low": "5 GHz (low)",
47
+ "band_5GHz_high": "5 GHz (high)",
48
+ "band_6GHz": "6 GHz",
49
+ }
50
+ return band_map.get(band, band or "Unknown")
51
+
52
+
53
+ def _get_neighbor_location(neighbor: Any) -> Optional[str]:
54
+ """Extract location from a neighbor object."""
55
+ if not neighbor:
56
+ return None
57
+
58
+ if hasattr(neighbor, "metadata"):
59
+ return getattr(neighbor.metadata, "location", None)
60
+ elif isinstance(neighbor, dict):
61
+ metadata = neighbor.get("metadata", {})
62
+ return metadata.get("location") if metadata else None
63
+
64
+ return None
65
+
66
+
67
+ # ==================== Eero Table ====================
68
+
69
+
70
+ def create_eeros_table(eeros: List[Eero]) -> Table:
71
+ """Create a table displaying Eero devices.
72
+
73
+ Args:
74
+ eeros: List of Eero objects
75
+
76
+ Returns:
77
+ Rich Table object
78
+ """
79
+ table = Table(title="Eero Devices")
80
+ table.add_column("ID", style="cyan", no_wrap=True)
81
+ table.add_column("Name", style="magenta")
82
+ table.add_column("Model", style="green")
83
+ table.add_column("IP", style="blue")
84
+ table.add_column("Status", style="yellow")
85
+ table.add_column("Type", style="red")
86
+ table.add_column("Role", style="white")
87
+ table.add_column("Connection", style="green")
88
+
89
+ for eero in eeros:
90
+ eero_id = eero.eero_id if hasattr(eero, "eero_id") else ""
91
+ eero_name = str(eero.location) if eero.location else ""
92
+ device_type = "Primary" if eero.is_primary_node else "Secondary"
93
+ role = "Gateway" if eero.gateway else "Leaf"
94
+ _, status_style = format_eero_status(eero.status)
95
+
96
+ table.add_row(
97
+ eero_id,
98
+ eero_name,
99
+ eero.model,
100
+ eero.ip_address or "",
101
+ f"[{status_style}]{eero.status}[/{status_style}]",
102
+ device_type,
103
+ role,
104
+ eero.connection_type or "Unknown",
105
+ )
106
+
107
+ return table
108
+
109
+
110
+ # ==================== Eero Brief View Panels ====================
111
+
112
+
113
+ def _eero_basic_panel(eero: Eero, extensive: bool = False) -> Panel:
114
+ """Build the basic eero info panel."""
115
+ eero_id = eero.eero_id if hasattr(eero, "eero_id") else "Unknown"
116
+ eero_name = str(eero.location) if eero.location else ""
117
+ role = "Gateway" if eero.gateway else "Leaf"
118
+ device_type = "Primary" if eero.is_primary_node else "Secondary"
119
+ _, status_style = format_eero_status(eero.status)
120
+
121
+ lines = [
122
+ field("ID", eero_id),
123
+ field("Name", eero_name),
124
+ field("Model", eero.model),
125
+ ]
126
+
127
+ if extensive:
128
+ lines.extend(
129
+ [
130
+ field("Model Number", getattr(eero, "model_number", None)),
131
+ field("Model Variant", getattr(eero, "model_variant", None), "N/A"),
132
+ ]
133
+ )
134
+
135
+ lines.extend(
136
+ [
137
+ field("Serial", eero.serial),
138
+ field("MAC Address", eero.mac_address),
139
+ field("IP Address", eero.ip_address),
140
+ field_status("Status", eero.status, status_style),
141
+ ]
142
+ )
143
+
144
+ state = getattr(eero, "state", None)
145
+ if state:
146
+ state_style = "green" if state == "ONLINE" else "red"
147
+ lines.append(field_status("State", state, state_style))
148
+
149
+ lines.extend(
150
+ [
151
+ field("Type", device_type),
152
+ field("Role", role),
153
+ field("Connection", eero.connection_type),
154
+ ]
155
+ )
156
+
157
+ if extensive:
158
+ lines.extend(
159
+ [
160
+ field_bool("Wired", getattr(eero, "wired", False)),
161
+ field_bool("Using WAN", getattr(eero, "using_wan", False)),
162
+ ]
163
+ )
164
+ else:
165
+ mesh_quality = getattr(eero, "mesh_quality_bars", None)
166
+ if mesh_quality is not None:
167
+ quality_style = (
168
+ "green" if mesh_quality >= 4 else "yellow" if mesh_quality >= 2 else "red"
169
+ )
170
+ filled = "●" * mesh_quality
171
+ empty = "○" * (5 - mesh_quality)
172
+ lines.append(
173
+ f"[bold]Mesh Quality:[/bold] [{quality_style}]{filled}{empty} "
174
+ f"({mesh_quality}/5)[/{quality_style}]"
175
+ )
176
+
177
+ lines.extend(
178
+ [
179
+ field("Connected Clients", eero.connected_clients_count),
180
+ field("Firmware", eero.os),
181
+ field("Uptime", f"{eero.uptime or 0} days"),
182
+ ]
183
+ )
184
+
185
+ if getattr(eero, "update_available", False):
186
+ lines.append("[bold]Update:[/bold] [yellow]Update available[/yellow]")
187
+
188
+ return build_panel(lines, f"Eero: {eero_name}", "blue")
189
+
190
+
191
+ def _eero_connectivity_panel(eero: Eero) -> Panel:
192
+ """Build the connectivity panel for brief view."""
193
+ wired_clients = getattr(eero, "connected_wired_clients_count", 0)
194
+ wireless_clients = getattr(eero, "connected_wireless_clients_count", 0)
195
+ total_clients = eero.connected_clients_count or 0
196
+
197
+ lines = [
198
+ field("Total Clients", total_clients),
199
+ field("Wired Clients", wired_clients),
200
+ field("Wireless Clients", wireless_clients),
201
+ ]
202
+
203
+ led_on = getattr(eero, "led_on", None)
204
+ if led_on is not None:
205
+ led_brightness = getattr(eero, "led_brightness", 0)
206
+ led_status = f"On ({led_brightness}%)" if led_on else "Off"
207
+ led_style = "green" if led_on else "dim"
208
+ lines.append(f"[bold]LED:[/bold] [{led_style}]{led_status}[/{led_style}]")
209
+
210
+ provides_wifi = getattr(eero, "provides_wifi", None)
211
+ if provides_wifi is not None:
212
+ lines.append(field_bool("Provides WiFi", provides_wifi))
213
+
214
+ return build_panel(lines, "Connectivity", "green")
215
+
216
+
217
+ def _eero_bands_panel(eero: Eero) -> Optional[Panel]:
218
+ """Build the WiFi bands panel for brief view."""
219
+ bands = getattr(eero, "bands", [])
220
+ if not bands:
221
+ return None
222
+
223
+ formatted_bands = [_format_band(band) for band in bands]
224
+ lines = ["[bold]Supported Bands:[/bold]"] + [f" • {band}" for band in formatted_bands]
225
+ return build_panel(lines, "WiFi Bands", "cyan")
226
+
227
+
228
+ def _eero_ethernet_ports_panel(eero: Eero) -> Optional[Panel]:
229
+ """Build the ethernet ports panel for brief view."""
230
+ ethernet_status = getattr(eero, "ethernet_status", None)
231
+ if not ethernet_status or not hasattr(ethernet_status, "statuses"):
232
+ return None
233
+
234
+ statuses = getattr(ethernet_status, "statuses", [])
235
+ if not statuses:
236
+ return None
237
+
238
+ lines = []
239
+ for port in statuses:
240
+ port_name = getattr(port, "port_name", None) or "?"
241
+ has_carrier = getattr(port, "has_carrier", False) or getattr(port, "hasCarrier", False)
242
+ speed = getattr(port, "speed", None)
243
+ is_wan = getattr(port, "is_wan_port", False) or getattr(port, "isWanPort", False)
244
+ neighbor = getattr(port, "neighbor", None)
245
+
246
+ if has_carrier:
247
+ speed_str = _format_ethernet_speed(speed)
248
+ port_info = f"[green]●[/green] Port {port_name}: {speed_str}"
249
+
250
+ if is_wan:
251
+ port_info += " [cyan](WAN)[/cyan]"
252
+
253
+ neighbor_location = _get_neighbor_location(neighbor)
254
+ if neighbor_location:
255
+ port_info += f" → {neighbor_location}"
256
+ else:
257
+ port_info = f"[dim]○[/dim] Port {port_name}: [dim]Disconnected[/dim]"
258
+
259
+ lines.append(port_info)
260
+
261
+ return build_panel(lines, "Ethernet Ports", "yellow") if lines else None
262
+
263
+
264
+ def _eero_timing_brief_panel(eero: Eero) -> Optional[Panel]:
265
+ """Build the timing panel for brief view."""
266
+ last_reboot = getattr(eero, "last_reboot", None)
267
+ last_heartbeat = getattr(eero, "last_heartbeat", None)
268
+ joined = getattr(eero, "joined", None)
269
+ heartbeat_ok = getattr(eero, "heartbeat_ok", None)
270
+
271
+ if not (last_reboot or last_heartbeat or joined):
272
+ return None
273
+
274
+ lines = []
275
+
276
+ if last_reboot:
277
+ lines.append(field("Last Reboot", format_datetime(last_reboot)))
278
+
279
+ if last_heartbeat:
280
+ heartbeat_str = format_datetime(last_heartbeat)
281
+ hb_style = "green" if heartbeat_ok else "yellow"
282
+ lines.append(f"[bold]Last Heartbeat:[/bold] [{hb_style}]{heartbeat_str}[/{hb_style}]")
283
+
284
+ if joined:
285
+ lines.append(field("Joined Network", format_datetime(joined, include_time=False)))
286
+
287
+ return build_panel(lines, "Timing", "magenta")
288
+
289
+
290
+ def _eero_organization_panel(eero: Eero) -> Optional[Panel]:
291
+ """Build the organization panel for brief view."""
292
+ org_info = getattr(eero, "organization", None)
293
+ if not org_info:
294
+ return None
295
+
296
+ org_name = (
297
+ org_info.get("name") if isinstance(org_info, dict) else getattr(org_info, "name", None)
298
+ )
299
+
300
+ if not org_name:
301
+ return None
302
+
303
+ return build_panel([field("ISP/Organization", org_name)], "Organization", "blue")
304
+
305
+
306
+ def _eero_power_panel(eero: Eero) -> Optional[Panel]:
307
+ """Build the power info panel for brief view."""
308
+ power_info = getattr(eero, "power_info", None)
309
+ if not power_info:
310
+ return None
311
+
312
+ power_source = (
313
+ power_info.get("power_source")
314
+ if isinstance(power_info, dict)
315
+ else getattr(power_info, "power_source", None)
316
+ )
317
+
318
+ if not power_source:
319
+ return None
320
+
321
+ return build_panel([field("Power Source", power_source)], "Power", "yellow")
322
+
323
+
324
+ def _eero_location_panel(eero: Eero) -> Optional[Panel]:
325
+ """Build the location panel for brief view."""
326
+ if not eero.location:
327
+ return None
328
+
329
+ if isinstance(eero.location, str):
330
+ return build_panel([eero.location], "Location", "yellow")
331
+
332
+ if hasattr(eero.location, "address") and (eero.location.address or eero.location.city):
333
+ location_parts = []
334
+ if eero.location.address:
335
+ location_parts.append(eero.location.address)
336
+
337
+ city_state = []
338
+ if eero.location.city:
339
+ city_state.append(eero.location.city)
340
+ if eero.location.state:
341
+ city_state.append(eero.location.state)
342
+ if city_state:
343
+ location_parts.append(", ".join(city_state))
344
+
345
+ if eero.location.zip_code:
346
+ location_parts.append(eero.location.zip_code)
347
+ if eero.location.country:
348
+ location_parts.append(eero.location.country)
349
+
350
+ return build_panel(location_parts, "Location", "yellow")
351
+
352
+ return None
353
+
354
+
355
+ # ==================== Eero Extensive View Panels ====================
356
+
357
+
358
+ def _eero_network_info_panel(eero: Eero) -> Optional[Panel]:
359
+ """Build the network info panel for extensive view."""
360
+ network_info = getattr(eero, "network", {})
361
+ if not network_info:
362
+ return None
363
+
364
+ lines = [
365
+ field("Network Name", network_info.get("name")),
366
+ field("Network URL", network_info.get("url")),
367
+ field("Network Created", network_info.get("created")),
368
+ ]
369
+ return build_panel(lines, "Network Information", "green")
370
+
371
+
372
+ def _eero_timing_extensive_panel(eero: Eero) -> Panel:
373
+ """Build the timing panel for extensive view."""
374
+ lines = [
375
+ field("Joined", getattr(eero, "joined", None)),
376
+ field("Last Reboot", getattr(eero, "last_reboot", None)),
377
+ field("Last Heartbeat", getattr(eero, "last_heartbeat", None)),
378
+ field_bool("Heartbeat OK", getattr(eero, "heartbeat_ok", False)),
379
+ ]
380
+ return build_panel(lines, "Timing Information", "yellow")
381
+
382
+
383
+ def _eero_firmware_panel(eero: Eero) -> Panel:
384
+ """Build the firmware info panel for extensive view."""
385
+ lines = [
386
+ field("OS", eero.os),
387
+ field("OS Version", getattr(eero, "os_version", None)),
388
+ field_bool("Update Available", getattr(eero, "update_available", False)),
389
+ ]
390
+ return build_panel(lines, "Firmware Information", "cyan")
391
+
392
+
393
+ def _eero_update_status_panel(eero: Eero) -> Optional[Panel]:
394
+ """Build the update status panel for extensive view."""
395
+ update_status = getattr(eero, "update_status", None)
396
+ if not update_status:
397
+ return None
398
+
399
+ lines = [
400
+ field_bool("Support Expired", getattr(update_status, "support_expired", False)),
401
+ field(
402
+ "Support Expiration",
403
+ getattr(update_status, "support_expiration_string", None),
404
+ "N/A",
405
+ ),
406
+ ]
407
+ return build_panel(lines, "Update Status", "magenta")
408
+
409
+
410
+ def _eero_client_info_panel(eero: Eero) -> Panel:
411
+ """Build the client info panel for extensive view."""
412
+ lines = [
413
+ field("Connected Clients", eero.connected_clients_count),
414
+ field("Wired Clients", getattr(eero, "connected_wired_clients_count", 0)),
415
+ field("Wireless Clients", getattr(eero, "connected_wireless_clients_count", 0)),
416
+ ]
417
+ return build_panel(lines, "Client Information", "blue")
418
+
419
+
420
+ def _eero_led_panel(eero: Eero) -> Panel:
421
+ """Build the LED info panel for extensive view."""
422
+ lines = [
423
+ field_bool("LED On", getattr(eero, "led_on", False)),
424
+ field("LED Brightness", f"{getattr(eero, 'led_brightness', 0)}%"),
425
+ ]
426
+ return build_panel(lines, "LED Information", "green")
427
+
428
+
429
+ def _eero_mesh_panel(eero: Eero) -> Panel:
430
+ """Build the mesh info panel for extensive view."""
431
+ lines = [
432
+ field("Mesh Quality", f"{getattr(eero, 'mesh_quality_bars', 0)}/5"),
433
+ field_bool("Auto Provisioned", getattr(eero, "auto_provisioned", False)),
434
+ field_bool("Provides WiFi", getattr(eero, "provides_wifi", False)),
435
+ ]
436
+ return build_panel(lines, "Mesh Information", "cyan")
437
+
438
+
439
+ def _eero_wifi_bssids_panel(eero: Eero) -> Optional[Panel]:
440
+ """Build the WiFi BSSIDs panel for extensive view."""
441
+ wifi_bssids = getattr(eero, "wifi_bssids", [])
442
+ if not wifi_bssids:
443
+ return None
444
+
445
+ lines = ["[bold]WiFi BSSIDs:[/bold]"] + [f" • {bssid}" for bssid in wifi_bssids]
446
+ return build_panel(lines, "WiFi BSSIDs", "magenta")
447
+
448
+
449
+ def _eero_bands_extensive_panel(eero: Eero) -> Optional[Panel]:
450
+ """Build the bands panel for extensive view."""
451
+ bands = getattr(eero, "bands", [])
452
+ if not bands:
453
+ return None
454
+
455
+ lines = ["[bold]Supported Bands:[/bold]"] + [f" • {band}" for band in bands]
456
+ return build_panel(lines, "Supported Bands", "blue")
457
+
458
+
459
+ def _eero_ethernet_addresses_panel(eero: Eero) -> Optional[Panel]:
460
+ """Build the ethernet addresses panel for extensive view."""
461
+ ethernet_addresses = getattr(eero, "ethernet_addresses", [])
462
+ if not ethernet_addresses:
463
+ return None
464
+
465
+ lines = ["[bold]Ethernet Addresses:[/bold]"] + [f" • {addr}" for addr in ethernet_addresses]
466
+ return build_panel(lines, "Ethernet Addresses", "yellow")
467
+
468
+
469
+ def _eero_port_details_panel(eero: Eero) -> Optional[Panel]:
470
+ """Build the port details panel for extensive view."""
471
+ port_details = getattr(eero, "port_details", [])
472
+ if not port_details:
473
+ return None
474
+
475
+ lines = ["[bold]Port Details:[/bold]"] + [
476
+ f" • Port {getattr(item, 'port_name', 'Unknown')} "
477
+ f"({getattr(item, 'position', 'Unknown')}): "
478
+ f"{getattr(item, 'ethernet_address', 'Unknown')}"
479
+ for item in port_details
480
+ ]
481
+ return build_panel(lines, "Port Details", "cyan")
482
+
483
+
484
+ def _eero_resources_panel(eero: Eero) -> Optional[Panel]:
485
+ """Build the resources panel for extensive view."""
486
+ resources = getattr(eero, "resources", {})
487
+ if not resources:
488
+ return None
489
+
490
+ lines = ["[bold]Available Resources:[/bold]"] + [
491
+ f" • {key}: {value}" for key, value in resources.items()
492
+ ]
493
+ return build_panel(lines, "Available Resources", "blue")
494
+
495
+
496
+ def _eero_messages_panel(eero: Eero) -> Optional[Panel]:
497
+ """Build the messages panel for extensive view."""
498
+ messages = getattr(eero, "messages", [])
499
+ if not messages:
500
+ return None
501
+
502
+ lines = ["[bold]Messages:[/bold]"] + [f" • {msg}" for msg in messages]
503
+ return build_panel(lines, "Messages", "red")
504
+
505
+
506
+ def _eero_health_panel(eero: Eero) -> Optional[Panel]:
507
+ """Build the health issues panel (shown if issues exist)."""
508
+ if not eero.health or not eero.health.issues:
509
+ return None
510
+
511
+ lines = [
512
+ f"[bold]{issue.get('type', 'Issue')}:[/bold] {issue.get('description', 'No description')}"
513
+ for issue in eero.health.issues
514
+ ]
515
+ return build_panel(lines, "Health Issues", "red")
516
+
517
+
518
+ # ==================== Main Eero Details Function ====================
519
+
520
+
521
+ def print_eero_details(eero: Eero, detail_level: DetailLevel = "brief") -> None:
522
+ """Print Eero device information with configurable detail level.
523
+
524
+ Args:
525
+ eero: Eero object
526
+ detail_level: "brief" or "full"
527
+ """
528
+ extensive = detail_level == "full"
529
+
530
+ # Basic info panel (always shown)
531
+ console.print(_eero_basic_panel(eero, extensive))
532
+
533
+ if not extensive:
534
+ # Brief view panels
535
+ console.print(_eero_connectivity_panel(eero))
536
+
537
+ # Optional brief panels
538
+ for panel_func in [
539
+ _eero_bands_panel,
540
+ _eero_ethernet_ports_panel,
541
+ _eero_timing_brief_panel,
542
+ _eero_organization_panel,
543
+ _eero_power_panel,
544
+ _eero_location_panel,
545
+ ]:
546
+ panel = panel_func(eero)
547
+ if panel:
548
+ console.print(panel)
549
+ else:
550
+ # Extensive view panels
551
+ for panel_func in [
552
+ _eero_network_info_panel,
553
+ ]:
554
+ panel = panel_func(eero)
555
+ if panel:
556
+ console.print(panel)
557
+
558
+ # Required extensive panels
559
+ console.print(_eero_timing_extensive_panel(eero))
560
+ console.print(_eero_firmware_panel(eero))
561
+
562
+ # Optional extensive panels
563
+ for panel_func in [
564
+ _eero_update_status_panel,
565
+ ]:
566
+ panel = panel_func(eero)
567
+ if panel:
568
+ console.print(panel)
569
+
570
+ # Required extensive panels
571
+ console.print(_eero_client_info_panel(eero))
572
+ console.print(_eero_led_panel(eero))
573
+ console.print(_eero_mesh_panel(eero))
574
+
575
+ # Optional extensive panels
576
+ for panel_func in [
577
+ _eero_wifi_bssids_panel,
578
+ _eero_bands_extensive_panel,
579
+ _eero_ethernet_addresses_panel,
580
+ _eero_port_details_panel,
581
+ _eero_resources_panel,
582
+ _eero_messages_panel,
583
+ ]:
584
+ panel = panel_func(eero)
585
+ if panel:
586
+ console.print(panel)
587
+
588
+ # Health issues (always shown if present, regardless of detail level)
589
+ health_panel = _eero_health_panel(eero)
590
+ if health_panel:
591
+ console.print(health_panel)
@@ -0,0 +1,87 @@
1
+ """Miscellaneous formatting utilities for the Eero CLI.
2
+
3
+ This module provides formatting functions for speed tests, blacklists,
4
+ and other miscellaneous data.
5
+ """
6
+
7
+ from typing import Any, Dict, List
8
+
9
+ from rich.table import Table
10
+
11
+ from .base import build_panel, console, field
12
+
13
+ # ==================== Speed Test Formatting ====================
14
+
15
+
16
+ def print_speedtest_results(result: dict) -> None:
17
+ """Print speed test results.
18
+
19
+ Args:
20
+ result: Speed test result dictionary
21
+ """
22
+ download = result.get("down", {}).get("value", 0)
23
+ upload = result.get("up", {}).get("value", 0)
24
+ latency = result.get("latency", {}).get("value", 0)
25
+
26
+ lines = [
27
+ field("Download", f"{download} Mbps"),
28
+ field("Upload", f"{upload} Mbps"),
29
+ field("Latency", f"{latency} ms"),
30
+ ]
31
+ console.print(build_panel(lines, "Speed Test Results", "green"))
32
+
33
+
34
+ # ==================== Blacklist Formatting ====================
35
+
36
+
37
+ def create_blacklist_table(blacklist_data: List[Dict[str, Any]]) -> Table:
38
+ """Create a table displaying blacklisted devices.
39
+
40
+ Args:
41
+ blacklist_data: List of blacklisted device dictionaries
42
+
43
+ Returns:
44
+ Rich Table object
45
+ """
46
+ table = Table(title="Blacklisted Devices")
47
+ table.add_column("ID", style="dim")
48
+ table.add_column("Name", style="cyan")
49
+ table.add_column("Nickname", style="blue")
50
+ table.add_column("IP", style="green")
51
+ table.add_column("MAC", style="yellow")
52
+ table.add_column("Type", style="cyan")
53
+ table.add_column("Manufacturer", style="green")
54
+ table.add_column("Connection Type", style="blue")
55
+ table.add_column("Eero Location", style="yellow")
56
+ table.add_column("Last Active", style="magenta")
57
+
58
+ for device in blacklist_data:
59
+ device_id = "Unknown"
60
+ if device.get("url"):
61
+ parts = device["url"].split("/")
62
+ if len(parts) >= 2:
63
+ device_id = parts[-1]
64
+
65
+ device_name = (
66
+ device.get("display_name")
67
+ or device.get("hostname")
68
+ or device.get("nickname")
69
+ or "Unknown"
70
+ )
71
+ source = device.get("source", {})
72
+ eero_location = source.get("location") if source else "Unknown"
73
+
74
+ table.add_row(
75
+ device_id,
76
+ device_name,
77
+ device.get("nickname") or "",
78
+ device.get("ip") or "Unknown",
79
+ device.get("mac") or "Unknown",
80
+ device.get("device_type") or "Unknown",
81
+ device.get("manufacturer") or "Unknown",
82
+ device.get("connection_type") or "Unknown",
83
+ eero_location,
84
+ str(device.get("last_active") or "Unknown"),
85
+ )
86
+
87
+ return table