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,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)
|