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,492 @@
1
+ """Device (connected device) commands for the Eero CLI.
2
+
3
+ Commands:
4
+ - eero device list: List all connected devices
5
+ - eero device show: Show device details
6
+ - eero device rename: Rename a device
7
+ - eero device block: Block a device
8
+ - eero device unblock: Unblock a device
9
+ - eero device priority: Device priority management
10
+ """
11
+
12
+ import asyncio
13
+ import sys
14
+ from typing import Literal, Optional
15
+
16
+ import click
17
+ from eero import EeroClient
18
+ from eero.const import EeroDeviceStatus
19
+ from rich.panel import Panel
20
+ from rich.table import Table
21
+
22
+ from ..context import EeroCliContext, ensure_cli_context
23
+ from ..exit_codes import ExitCode
24
+ from ..options import apply_options, force_option, network_option, output_option
25
+ from ..output import OutputFormat
26
+ from ..safety import OperationRisk, SafetyError, confirm_or_fail
27
+ from ..utils import run_with_client
28
+
29
+
30
+ @click.group(name="device")
31
+ @click.pass_context
32
+ def device_group(ctx: click.Context) -> None:
33
+ """Manage connected devices.
34
+
35
+ \b
36
+ Commands:
37
+ list - List all connected devices
38
+ show - Show device details
39
+ rename - Rename a device
40
+ block - Block a device
41
+ unblock - Unblock a device
42
+ priority - Bandwidth priority management
43
+
44
+ \b
45
+ Examples:
46
+ eero device list # List all devices
47
+ eero device show "iPhone" # Show by name
48
+ eero device block AA:BB:CC:DD:EE:FF # Block by MAC
49
+ eero device priority show "iPad" # Show priority
50
+ """
51
+ ensure_cli_context(ctx)
52
+
53
+
54
+ @device_group.command(name="list")
55
+ @output_option
56
+ @network_option
57
+ @click.pass_context
58
+ def device_list(ctx: click.Context, output: Optional[str], network_id: Optional[str]) -> None:
59
+ """List all connected devices."""
60
+ cli_ctx = apply_options(ctx, output=output, network_id=network_id)
61
+ console = cli_ctx.console
62
+
63
+ async def run_cmd() -> None:
64
+ async def get_devices(client: EeroClient) -> None:
65
+ with cli_ctx.status("Getting devices..."):
66
+ devices = await client.get_devices(cli_ctx.network_id)
67
+
68
+ if not devices:
69
+ console.print("[yellow]No devices found[/yellow]")
70
+ return
71
+
72
+ if cli_ctx.is_structured_output():
73
+ data = [d.model_dump(mode="json") for d in devices]
74
+ cli_ctx.render_structured(data, "eero.device.list/v1")
75
+ elif cli_ctx.output_format == OutputFormat.LIST:
76
+ for d in devices:
77
+ name = d.display_name or d.hostname or d.nickname or "Unknown"
78
+ status = d.status.value if d.status else "unknown"
79
+ device_type = d.device_type or ""
80
+ connection = d.connection_type or ""
81
+ # Use print() with fixed-width columns for alignment
82
+ print(
83
+ f"{d.id or '':<14} {name:<30} {d.ip or d.ipv4 or '':<15} "
84
+ f"{d.mac or '':<17} {status:<12} {device_type:<20} {connection}"
85
+ )
86
+ else:
87
+ table = Table(title="Connected Devices")
88
+ table.add_column("ID", style="dim")
89
+ table.add_column("Name", style="cyan")
90
+ table.add_column("IP", style="green")
91
+ table.add_column("MAC", style="yellow")
92
+ table.add_column("Status")
93
+ table.add_column("Type")
94
+ table.add_column("Connection")
95
+
96
+ for d in devices:
97
+ name = d.display_name or d.hostname or d.nickname or "Unknown"
98
+
99
+ if d.status == EeroDeviceStatus.CONNECTED:
100
+ status = "[green]connected[/green]"
101
+ elif d.status == EeroDeviceStatus.BLOCKED:
102
+ status = "[red]blocked[/red]"
103
+ else:
104
+ status = "[yellow]disconnected[/yellow]"
105
+
106
+ table.add_row(
107
+ d.id or "",
108
+ name,
109
+ d.ip or d.ipv4 or "",
110
+ d.mac or "",
111
+ status,
112
+ d.device_type or "",
113
+ d.connection_type or "",
114
+ )
115
+
116
+ console.print(table)
117
+
118
+ await run_with_client(get_devices)
119
+
120
+ asyncio.run(run_cmd())
121
+
122
+
123
+ @device_group.command(name="show")
124
+ @click.argument("device_id")
125
+ @output_option
126
+ @network_option
127
+ @click.pass_context
128
+ def device_show(
129
+ ctx: click.Context, device_id: str, output: Optional[str], network_id: Optional[str]
130
+ ) -> None:
131
+ """Show details of a specific device.
132
+
133
+ \b
134
+ Arguments:
135
+ DEVICE_ID Device ID, MAC address, or name
136
+ """
137
+ cli_ctx = apply_options(ctx, output=output, network_id=network_id)
138
+ console = cli_ctx.console
139
+
140
+ async def run_cmd() -> None:
141
+ async def get_device(client: EeroClient) -> None:
142
+ with cli_ctx.status("Getting devices..."):
143
+ devices = await client.get_devices(cli_ctx.network_id)
144
+
145
+ # Find device by ID, MAC, or name
146
+ target = None
147
+ for d in devices:
148
+ if (
149
+ d.id == device_id
150
+ or d.mac == device_id
151
+ or d.display_name == device_id
152
+ or d.nickname == device_id
153
+ or d.hostname == device_id
154
+ ):
155
+ target = d
156
+ break
157
+
158
+ if not target or not target.id:
159
+ console.print(f"[red]Device '{device_id}' not found[/red]")
160
+ sys.exit(ExitCode.NOT_FOUND)
161
+
162
+ # Get full details
163
+ with cli_ctx.status("Getting device details..."):
164
+ device = await client.get_device(target.id, cli_ctx.network_id)
165
+
166
+ if cli_ctx.is_structured_output():
167
+ cli_ctx.render_structured(device.model_dump(mode="json"), "eero.device.show/v1")
168
+ else:
169
+ from ..formatting import print_device_details
170
+
171
+ detail: Literal["brief", "full"] = (
172
+ "full" if cli_ctx.detail_level == "full" else "brief"
173
+ )
174
+ print_device_details(device, detail_level=detail)
175
+
176
+ await run_with_client(get_device)
177
+
178
+ asyncio.run(run_cmd())
179
+
180
+
181
+ @device_group.command(name="rename")
182
+ @click.argument("device_id")
183
+ @click.option("--name", required=True, help="New nickname for the device")
184
+ @network_option
185
+ @click.pass_context
186
+ def device_rename(ctx: click.Context, device_id: str, name: str, network_id: Optional[str]) -> None:
187
+ """Rename a device.
188
+
189
+ \b
190
+ Arguments:
191
+ DEVICE_ID Device ID, MAC address, or name
192
+
193
+ \b
194
+ Options:
195
+ --name TEXT New nickname (required)
196
+ """
197
+ cli_ctx = apply_options(ctx, network_id=network_id)
198
+ console = cli_ctx.console
199
+
200
+ async def run_cmd() -> None:
201
+ async def rename_device(client: EeroClient) -> None:
202
+ # Find device first
203
+ with cli_ctx.status("Finding device..."):
204
+ devices = await client.get_devices(cli_ctx.network_id)
205
+
206
+ target = None
207
+ for d in devices:
208
+ if (
209
+ d.id == device_id
210
+ or d.mac == device_id
211
+ or d.display_name == device_id
212
+ or d.nickname == device_id
213
+ ):
214
+ target = d
215
+ break
216
+
217
+ if not target or not target.id:
218
+ console.print(f"[red]Device '{device_id}' not found[/red]")
219
+ sys.exit(ExitCode.NOT_FOUND)
220
+
221
+ with cli_ctx.status(f"Renaming device to '{name}'..."):
222
+ result = await client.set_device_nickname(target.id, name, cli_ctx.network_id)
223
+
224
+ if result:
225
+ console.print(f"[bold green]Device renamed to '{name}'[/bold green]")
226
+ else:
227
+ console.print("[red]Failed to rename device[/red]")
228
+ sys.exit(ExitCode.GENERIC_ERROR)
229
+
230
+ await run_with_client(rename_device)
231
+
232
+ asyncio.run(run_cmd())
233
+
234
+
235
+ @device_group.command(name="block")
236
+ @click.argument("device_id")
237
+ @force_option
238
+ @network_option
239
+ @click.pass_context
240
+ def device_block(
241
+ ctx: click.Context, device_id: str, force: Optional[bool], network_id: Optional[str]
242
+ ) -> None:
243
+ """Block a device from the network.
244
+
245
+ \b
246
+ Arguments:
247
+ DEVICE_ID Device ID, MAC address, or name
248
+ """
249
+ cli_ctx = apply_options(ctx, network_id=network_id, force=force)
250
+ _set_device_blocked(cli_ctx, device_id, True)
251
+
252
+
253
+ @device_group.command(name="unblock")
254
+ @click.argument("device_id")
255
+ @force_option
256
+ @network_option
257
+ @click.pass_context
258
+ def device_unblock(
259
+ ctx: click.Context, device_id: str, force: Optional[bool], network_id: Optional[str]
260
+ ) -> None:
261
+ """Unblock a device.
262
+
263
+ \b
264
+ Arguments:
265
+ DEVICE_ID Device ID, MAC address, or name
266
+ """
267
+ cli_ctx = apply_options(ctx, network_id=network_id, force=force)
268
+ _set_device_blocked(cli_ctx, device_id, False)
269
+
270
+
271
+ def _set_device_blocked(cli_ctx: EeroCliContext, device_id: str, blocked: bool) -> None:
272
+ """Block or unblock a device."""
273
+ console = cli_ctx.console
274
+ action = "block" if blocked else "unblock"
275
+
276
+ async def run_cmd() -> None:
277
+ async def toggle_block(client: EeroClient) -> None:
278
+ # Find device first
279
+ with cli_ctx.status("Finding device..."):
280
+ devices = await client.get_devices(cli_ctx.network_id)
281
+
282
+ target = None
283
+ for d in devices:
284
+ if (
285
+ d.id == device_id
286
+ or d.mac == device_id
287
+ or d.display_name == device_id
288
+ or d.nickname == device_id
289
+ ):
290
+ target = d
291
+ break
292
+
293
+ if not target or not target.id:
294
+ console.print(f"[red]Device '{device_id}' not found[/red]")
295
+ sys.exit(ExitCode.NOT_FOUND)
296
+
297
+ device_name = target.display_name or target.nickname or target.hostname or device_id
298
+
299
+ try:
300
+ confirm_or_fail(
301
+ action=action,
302
+ target=device_name,
303
+ risk=OperationRisk.MEDIUM,
304
+ force=cli_ctx.force,
305
+ non_interactive=cli_ctx.non_interactive,
306
+ dry_run=cli_ctx.dry_run,
307
+ console=cli_ctx.console,
308
+ )
309
+ except SafetyError as e:
310
+ cli_ctx.renderer.render_error(e.message)
311
+ sys.exit(e.exit_code)
312
+
313
+ with cli_ctx.status(f"{action.capitalize()}ing {device_name}..."):
314
+ result = await client.block_device(target.id, blocked, cli_ctx.network_id)
315
+
316
+ if result:
317
+ console.print(f"[bold green]Device {action}ed[/bold green]")
318
+ else:
319
+ console.print(f"[red]Failed to {action} device[/red]")
320
+ sys.exit(ExitCode.GENERIC_ERROR)
321
+
322
+ await run_with_client(toggle_block)
323
+
324
+ asyncio.run(run_cmd())
325
+
326
+
327
+ # ==================== Priority Subcommand Group ====================
328
+
329
+
330
+ @device_group.group(name="priority")
331
+ @click.pass_context
332
+ def priority_group(ctx: click.Context) -> None:
333
+ """Manage device bandwidth priority.
334
+
335
+ \b
336
+ Commands:
337
+ show - Show priority status
338
+ on - Enable priority
339
+ off - Disable priority
340
+ """
341
+ pass
342
+
343
+
344
+ @priority_group.command(name="show")
345
+ @click.argument("device_id")
346
+ @output_option
347
+ @network_option
348
+ @click.pass_context
349
+ def priority_show(
350
+ ctx: click.Context, device_id: str, output: Optional[str], network_id: Optional[str]
351
+ ) -> None:
352
+ """Show priority status for a device."""
353
+ cli_ctx = apply_options(ctx, output=output, network_id=network_id)
354
+ console = cli_ctx.console
355
+ renderer = cli_ctx.renderer
356
+
357
+ async def run_cmd() -> None:
358
+ async def get_priority(client: EeroClient) -> None:
359
+ # Find device first
360
+ with cli_ctx.status("Finding device..."):
361
+ devices = await client.get_devices(cli_ctx.network_id)
362
+
363
+ target = None
364
+ for d in devices:
365
+ if (
366
+ d.id == device_id
367
+ or d.mac == device_id
368
+ or d.display_name == device_id
369
+ or d.nickname == device_id
370
+ ):
371
+ target = d
372
+ break
373
+
374
+ if not target or not target.id:
375
+ console.print(f"[red]Device '{device_id}' not found[/red]")
376
+ sys.exit(ExitCode.NOT_FOUND)
377
+
378
+ with cli_ctx.status("Getting priority status..."):
379
+ priority_data = await client.get_device_priority(target.id, cli_ctx.network_id)
380
+
381
+ if cli_ctx.is_json_output():
382
+ renderer.render_json(priority_data, "eero.device.priority.show/v1")
383
+ else:
384
+ prioritized = priority_data.get("prioritized", False)
385
+ duration = priority_data.get("duration", 0)
386
+
387
+ content = f"[bold]Prioritized:[/bold] {'[green]Yes[/green]' if prioritized else '[dim]No[/dim]'}"
388
+ if prioritized and duration > 0:
389
+ content += f"\n[bold]Duration:[/bold] {duration} minutes"
390
+
391
+ console.print(Panel(content, title="Priority Status", border_style="blue"))
392
+
393
+ await run_with_client(get_priority)
394
+
395
+ asyncio.run(run_cmd())
396
+
397
+
398
+ @priority_group.command(name="on")
399
+ @click.argument("device_id")
400
+ @click.option("--minutes", "-m", type=int, default=0, help="Duration in minutes (0=indefinite)")
401
+ @network_option
402
+ @click.pass_context
403
+ def priority_on(
404
+ ctx: click.Context, device_id: str, minutes: int, network_id: Optional[str]
405
+ ) -> None:
406
+ """Enable priority for a device.
407
+
408
+ \b
409
+ Options:
410
+ --minutes, -m Duration (0=indefinite)
411
+ """
412
+ cli_ctx = apply_options(ctx, network_id=network_id)
413
+ console = cli_ctx.console
414
+
415
+ async def run_cmd() -> None:
416
+ async def set_priority(client: EeroClient) -> None:
417
+ # Find device first
418
+ with cli_ctx.status("Finding device..."):
419
+ devices = await client.get_devices(cli_ctx.network_id)
420
+
421
+ target = None
422
+ for d in devices:
423
+ if (
424
+ d.id == device_id
425
+ or d.mac == device_id
426
+ or d.display_name == device_id
427
+ or d.nickname == device_id
428
+ ):
429
+ target = d
430
+ break
431
+
432
+ if not target or not target.id:
433
+ console.print(f"[red]Device '{device_id}' not found[/red]")
434
+ sys.exit(ExitCode.NOT_FOUND)
435
+
436
+ duration_str = f" for {minutes} minutes" if minutes > 0 else " (indefinite)"
437
+ with cli_ctx.status(f"Prioritizing device{duration_str}..."):
438
+ result = await client.prioritize_device(target.id, minutes, cli_ctx.network_id)
439
+
440
+ if result:
441
+ console.print(f"[bold green]Device prioritized{duration_str}[/bold green]")
442
+ else:
443
+ console.print("[red]Failed to prioritize device[/red]")
444
+ sys.exit(ExitCode.GENERIC_ERROR)
445
+
446
+ await run_with_client(set_priority)
447
+
448
+ asyncio.run(run_cmd())
449
+
450
+
451
+ @priority_group.command(name="off")
452
+ @click.argument("device_id")
453
+ @network_option
454
+ @click.pass_context
455
+ def priority_off(ctx: click.Context, device_id: str, network_id: Optional[str]) -> None:
456
+ """Remove priority from a device."""
457
+ cli_ctx = apply_options(ctx, network_id=network_id)
458
+ console = cli_ctx.console
459
+
460
+ async def run_cmd() -> None:
461
+ async def remove_priority(client: EeroClient) -> None:
462
+ # Find device first
463
+ with cli_ctx.status("Finding device..."):
464
+ devices = await client.get_devices(cli_ctx.network_id)
465
+
466
+ target = None
467
+ for d in devices:
468
+ if (
469
+ d.id == device_id
470
+ or d.mac == device_id
471
+ or d.display_name == device_id
472
+ or d.nickname == device_id
473
+ ):
474
+ target = d
475
+ break
476
+
477
+ if not target or not target.id:
478
+ console.print(f"[red]Device '{device_id}' not found[/red]")
479
+ sys.exit(ExitCode.NOT_FOUND)
480
+
481
+ with cli_ctx.status("Removing priority..."):
482
+ result = await client.deprioritize_device(target.id, cli_ctx.network_id)
483
+
484
+ if result:
485
+ console.print("[bold green]Priority removed[/bold green]")
486
+ else:
487
+ console.print("[red]Failed to remove priority[/red]")
488
+ sys.exit(ExitCode.GENERIC_ERROR)
489
+
490
+ await run_with_client(remove_priority)
491
+
492
+ asyncio.run(run_cmd())
@@ -0,0 +1,12 @@
1
+ """Eero commands package for the Eero CLI.
2
+
3
+ This package contains all Eero mesh node-related commands, split into logical submodules:
4
+ - base: Core commands (list, show, reboot)
5
+ - led: LED management commands
6
+ - nightlight: Nightlight commands (Beacon only)
7
+ - updates: Update management commands
8
+ """
9
+
10
+ from .base import eero_group
11
+
12
+ __all__ = ["eero_group"]