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.
- eeroctl/__init__.py +19 -0
- eeroctl/commands/__init__.py +32 -0
- eeroctl/commands/activity.py +237 -0
- eeroctl/commands/auth.py +471 -0
- eeroctl/commands/completion.py +142 -0
- eeroctl/commands/device.py +492 -0
- eeroctl/commands/eero/__init__.py +12 -0
- eeroctl/commands/eero/base.py +224 -0
- eeroctl/commands/eero/led.py +154 -0
- eeroctl/commands/eero/nightlight.py +235 -0
- eeroctl/commands/eero/updates.py +82 -0
- eeroctl/commands/network/__init__.py +18 -0
- eeroctl/commands/network/advanced.py +191 -0
- eeroctl/commands/network/backup.py +162 -0
- eeroctl/commands/network/base.py +331 -0
- eeroctl/commands/network/dhcp.py +118 -0
- eeroctl/commands/network/dns.py +197 -0
- eeroctl/commands/network/forwards.py +115 -0
- eeroctl/commands/network/guest.py +162 -0
- eeroctl/commands/network/security.py +162 -0
- eeroctl/commands/network/speedtest.py +99 -0
- eeroctl/commands/network/sqm.py +194 -0
- eeroctl/commands/profile.py +671 -0
- eeroctl/commands/troubleshoot.py +317 -0
- eeroctl/context.py +254 -0
- eeroctl/errors.py +156 -0
- eeroctl/exit_codes.py +68 -0
- eeroctl/formatting/__init__.py +90 -0
- eeroctl/formatting/base.py +181 -0
- eeroctl/formatting/device.py +430 -0
- eeroctl/formatting/eero.py +591 -0
- eeroctl/formatting/misc.py +87 -0
- eeroctl/formatting/network.py +659 -0
- eeroctl/formatting/profile.py +443 -0
- eeroctl/main.py +161 -0
- eeroctl/options.py +429 -0
- eeroctl/output.py +739 -0
- eeroctl/safety.py +259 -0
- eeroctl/utils.py +181 -0
- eeroctl-1.7.1.dist-info/METADATA +115 -0
- eeroctl-1.7.1.dist-info/RECORD +45 -0
- eeroctl-1.7.1.dist-info/WHEEL +5 -0
- eeroctl-1.7.1.dist-info/entry_points.txt +3 -0
- eeroctl-1.7.1.dist-info/licenses/LICENSE +21 -0
- 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
|