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,118 @@
1
+ """DHCP commands for the Eero CLI.
2
+
3
+ Commands:
4
+ - eero network dhcp reservations: List DHCP reservations
5
+ - eero network dhcp leases: List current DHCP leases
6
+ """
7
+
8
+ import asyncio
9
+
10
+ import click
11
+ from eero import EeroClient
12
+ from rich.table import Table
13
+
14
+ from ...context import get_cli_context
15
+ from ...utils import run_with_client
16
+
17
+
18
+ @click.group(name="dhcp")
19
+ @click.pass_context
20
+ def dhcp_group(ctx: click.Context) -> None:
21
+ """Manage DHCP settings.
22
+
23
+ \b
24
+ Commands:
25
+ reservations - List DHCP reservations
26
+ leases - List current DHCP leases
27
+ reserve - Create a reservation (stub)
28
+ unreserve - Remove a reservation (stub)
29
+ """
30
+ pass
31
+
32
+
33
+ @dhcp_group.command(name="reservations")
34
+ @click.pass_context
35
+ def dhcp_reservations(ctx: click.Context) -> None:
36
+ """List DHCP reservations."""
37
+ cli_ctx = get_cli_context(ctx)
38
+ console = cli_ctx.console
39
+ renderer = cli_ctx.renderer
40
+
41
+ async def run_cmd() -> None:
42
+ async def get_reservations(client: EeroClient) -> None:
43
+ with cli_ctx.status("Getting DHCP reservations..."):
44
+ reservations = await client.get_reservations(cli_ctx.network_id)
45
+
46
+ if cli_ctx.is_json_output():
47
+ renderer.render_json(reservations, "eero.network.dhcp.reservations/v1")
48
+ else:
49
+ if not reservations:
50
+ console.print("[yellow]No DHCP reservations configured[/yellow]")
51
+ return
52
+
53
+ table = Table(title="DHCP Reservations")
54
+ table.add_column("MAC", style="yellow")
55
+ table.add_column("IP", style="green")
56
+ table.add_column("Hostname", style="cyan")
57
+
58
+ for res in reservations:
59
+ table.add_row(
60
+ res.get("mac", ""),
61
+ res.get("ip", ""),
62
+ res.get("hostname", ""),
63
+ )
64
+
65
+ console.print(table)
66
+
67
+ await run_with_client(get_reservations)
68
+
69
+ asyncio.run(run_cmd())
70
+
71
+
72
+ @dhcp_group.command(name="leases")
73
+ @click.pass_context
74
+ def dhcp_leases(ctx: click.Context) -> None:
75
+ """List current DHCP leases."""
76
+ cli_ctx = get_cli_context(ctx)
77
+ console = cli_ctx.console
78
+ renderer = cli_ctx.renderer
79
+
80
+ async def run_cmd() -> None:
81
+ async def get_leases(client: EeroClient) -> None:
82
+ with cli_ctx.status("Getting DHCP leases..."):
83
+ # Leases come from devices list
84
+ devices = await client.get_devices(cli_ctx.network_id)
85
+
86
+ if cli_ctx.is_json_output():
87
+ data = [
88
+ {
89
+ "ip": d.ip or d.ipv4,
90
+ "mac": d.mac,
91
+ "hostname": d.hostname,
92
+ "name": d.display_name or d.nickname,
93
+ }
94
+ for d in devices
95
+ if d.ip or d.ipv4
96
+ ]
97
+ renderer.render_json(data, "eero.network.dhcp.leases/v1")
98
+ else:
99
+ table = Table(title="DHCP Leases")
100
+ table.add_column("IP", style="green")
101
+ table.add_column("MAC", style="yellow")
102
+ table.add_column("Hostname", style="cyan")
103
+ table.add_column("Name", style="blue")
104
+
105
+ for d in devices:
106
+ if d.ip or d.ipv4:
107
+ table.add_row(
108
+ d.ip or d.ipv4 or "",
109
+ d.mac or "",
110
+ d.hostname or "",
111
+ d.display_name or d.nickname or "",
112
+ )
113
+
114
+ console.print(table)
115
+
116
+ await run_with_client(get_leases)
117
+
118
+ asyncio.run(run_cmd())
@@ -0,0 +1,197 @@
1
+ """DNS commands for the Eero CLI.
2
+
3
+ Commands:
4
+ - eero network dns show: Show DNS settings
5
+ - eero network dns mode set: Set DNS mode
6
+ - eero network dns caching: Enable/disable DNS caching
7
+ """
8
+
9
+ import asyncio
10
+ import sys
11
+
12
+ import click
13
+ from eero import EeroClient
14
+ from rich.panel import Panel
15
+
16
+ from ...context import EeroCliContext, get_cli_context
17
+ from ...exit_codes import ExitCode
18
+ from ...safety import OperationRisk, SafetyError, confirm_or_fail
19
+ from ...utils import run_with_client
20
+
21
+
22
+ @click.group(name="dns")
23
+ @click.pass_context
24
+ def dns_group(ctx: click.Context) -> None:
25
+ """Manage DNS settings.
26
+
27
+ \b
28
+ Commands:
29
+ show - Show current DNS settings
30
+ mode - Set DNS mode
31
+ caching - Enable/disable DNS caching
32
+
33
+ \b
34
+ Examples:
35
+ eero network dns show
36
+ eero network dns mode set google
37
+ eero network dns caching enable
38
+ """
39
+ pass
40
+
41
+
42
+ @dns_group.command(name="show")
43
+ @click.pass_context
44
+ def dns_show(ctx: click.Context) -> None:
45
+ """Show current DNS settings."""
46
+ cli_ctx = get_cli_context(ctx)
47
+ console = cli_ctx.console
48
+ renderer = cli_ctx.renderer
49
+
50
+ async def run_cmd() -> None:
51
+ async def get_dns(client: EeroClient) -> None:
52
+ with cli_ctx.status("Getting DNS settings..."):
53
+ dns_data = await client.get_dns_settings(cli_ctx.network_id)
54
+
55
+ if cli_ctx.is_json_output():
56
+ renderer.render_json(dns_data, "eero.network.dns.show/v1")
57
+ else:
58
+ caching = dns_data.get("dns_caching", False)
59
+ mode = dns_data.get("dns_mode", "auto")
60
+ custom_dns = dns_data.get("custom_dns", [])
61
+
62
+ content = (
63
+ f"[bold]DNS Mode:[/bold] {mode}\n"
64
+ f"[bold]DNS Caching:[/bold] {'[green]Enabled[/green]' if caching else '[dim]Disabled[/dim]'}"
65
+ )
66
+ if custom_dns:
67
+ content += f"\n[bold]Custom DNS:[/bold] {', '.join(custom_dns)}"
68
+
69
+ console.print(Panel(content, title="DNS Settings", border_style="blue"))
70
+
71
+ await run_with_client(get_dns)
72
+
73
+ asyncio.run(run_cmd())
74
+
75
+
76
+ @dns_group.group(name="mode")
77
+ @click.pass_context
78
+ def dns_mode_group(ctx: click.Context) -> None:
79
+ """Set DNS mode."""
80
+ pass
81
+
82
+
83
+ @dns_mode_group.command(name="set")
84
+ @click.argument("mode", type=click.Choice(["auto", "cloudflare", "google", "opendns", "custom"]))
85
+ @click.option("--servers", "-s", multiple=True, help="Custom DNS servers (for 'custom' mode)")
86
+ @click.option("--force", "-f", is_flag=True, help="Skip confirmation")
87
+ @click.pass_context
88
+ def dns_mode_set(ctx: click.Context, mode: str, servers: tuple, force: bool) -> None:
89
+ """Set DNS mode.
90
+
91
+ \b
92
+ Modes:
93
+ auto - Use ISP DNS
94
+ cloudflare - Use Cloudflare (1.1.1.1)
95
+ google - Use Google (8.8.8.8)
96
+ opendns - Use OpenDNS
97
+ custom - Use custom servers (requires --servers)
98
+
99
+ \b
100
+ Examples:
101
+ eero network dns mode set google
102
+ eero network dns mode set custom --servers 8.8.8.8 --servers 8.8.4.4
103
+ """
104
+ cli_ctx = get_cli_context(ctx)
105
+ console = cli_ctx.console
106
+
107
+ if mode == "custom" and not servers:
108
+ console.print("[red]Error: --servers required for custom mode[/red]")
109
+ sys.exit(ExitCode.USAGE_ERROR)
110
+
111
+ try:
112
+ confirm_or_fail(
113
+ action="change DNS mode",
114
+ target=f"to {mode}",
115
+ risk=OperationRisk.MEDIUM,
116
+ force=force or cli_ctx.force,
117
+ non_interactive=cli_ctx.non_interactive,
118
+ dry_run=cli_ctx.dry_run,
119
+ )
120
+ except SafetyError as e:
121
+ cli_ctx.renderer.render_error(e.message)
122
+ sys.exit(e.exit_code)
123
+
124
+ async def run_cmd() -> None:
125
+ async def set_mode(client: EeroClient) -> None:
126
+ custom_servers = list(servers) if servers else None
127
+ with cli_ctx.status(f"Setting DNS mode to {mode}..."):
128
+ result = await client.set_dns_mode(mode, custom_servers, cli_ctx.network_id)
129
+
130
+ if result:
131
+ console.print(f"[bold green]DNS mode set to {mode}[/bold green]")
132
+ else:
133
+ console.print("[red]Failed to set DNS mode[/red]")
134
+ sys.exit(ExitCode.GENERIC_ERROR)
135
+
136
+ await run_with_client(set_mode)
137
+
138
+ asyncio.run(run_cmd())
139
+
140
+
141
+ @dns_group.group(name="caching")
142
+ @click.pass_context
143
+ def dns_caching_group(ctx: click.Context) -> None:
144
+ """Manage DNS caching."""
145
+ pass
146
+
147
+
148
+ @dns_caching_group.command(name="enable")
149
+ @click.option("--force", "-f", is_flag=True, help="Skip confirmation")
150
+ @click.pass_context
151
+ def dns_caching_enable(ctx: click.Context, force: bool) -> None:
152
+ """Enable DNS caching."""
153
+ cli_ctx = get_cli_context(ctx)
154
+ _set_dns_caching(cli_ctx, True, force)
155
+
156
+
157
+ @dns_caching_group.command(name="disable")
158
+ @click.option("--force", "-f", is_flag=True, help="Skip confirmation")
159
+ @click.pass_context
160
+ def dns_caching_disable(ctx: click.Context, force: bool) -> None:
161
+ """Disable DNS caching."""
162
+ cli_ctx = get_cli_context(ctx)
163
+ _set_dns_caching(cli_ctx, False, force)
164
+
165
+
166
+ def _set_dns_caching(cli_ctx: EeroCliContext, enable: bool, force: bool) -> None:
167
+ """Set DNS caching state."""
168
+ console = cli_ctx.console
169
+ action = "enable" if enable else "disable"
170
+
171
+ try:
172
+ confirm_or_fail(
173
+ action=f"{action} DNS caching",
174
+ target="network",
175
+ risk=OperationRisk.MEDIUM,
176
+ force=force or cli_ctx.force,
177
+ non_interactive=cli_ctx.non_interactive,
178
+ dry_run=cli_ctx.dry_run,
179
+ )
180
+ except SafetyError as e:
181
+ cli_ctx.renderer.render_error(e.message)
182
+ sys.exit(e.exit_code)
183
+
184
+ async def run_cmd() -> None:
185
+ async def set_caching(client: EeroClient) -> None:
186
+ with cli_ctx.status(f"{action.capitalize()}ing DNS caching..."):
187
+ result = await client.set_dns_caching(enable, cli_ctx.network_id)
188
+
189
+ if result:
190
+ console.print(f"[bold green]DNS caching {action}d[/bold green]")
191
+ else:
192
+ console.print(f"[red]Failed to {action} DNS caching[/red]")
193
+ sys.exit(ExitCode.GENERIC_ERROR)
194
+
195
+ await run_with_client(set_caching)
196
+
197
+ asyncio.run(run_cmd())
@@ -0,0 +1,115 @@
1
+ """Port forwarding commands for the Eero CLI.
2
+
3
+ Commands:
4
+ - eero network forwards list: List all port forwards
5
+ - eero network forwards show: Show details of a port forward
6
+ """
7
+
8
+ import asyncio
9
+ import sys
10
+
11
+ import click
12
+ from eero import EeroClient
13
+ from rich.panel import Panel
14
+ from rich.table import Table
15
+
16
+ from ...context import get_cli_context
17
+ from ...exit_codes import ExitCode
18
+ from ...utils import run_with_client
19
+
20
+
21
+ @click.group(name="forwards")
22
+ @click.pass_context
23
+ def forwards_group(ctx: click.Context) -> None:
24
+ """Manage port forwarding rules.
25
+
26
+ \b
27
+ Commands:
28
+ list - List all port forwards
29
+ show - Show details of a port forward
30
+ add - Add a new port forward (stub)
31
+ delete - Delete a port forward (stub)
32
+ """
33
+ pass
34
+
35
+
36
+ @forwards_group.command(name="list")
37
+ @click.pass_context
38
+ def forwards_list(ctx: click.Context) -> None:
39
+ """List all port forwarding rules."""
40
+ cli_ctx = get_cli_context(ctx)
41
+ console = cli_ctx.console
42
+ renderer = cli_ctx.renderer
43
+
44
+ async def run_cmd() -> None:
45
+ async def get_forwards(client: EeroClient) -> None:
46
+ with cli_ctx.status("Getting port forwards..."):
47
+ forwards = await client.get_forwards(cli_ctx.network_id)
48
+
49
+ if cli_ctx.is_json_output():
50
+ renderer.render_json(forwards, "eero.network.forwards.list/v1")
51
+ else:
52
+ if not forwards:
53
+ console.print("[yellow]No port forwards configured[/yellow]")
54
+ return
55
+
56
+ table = Table(title="Port Forwards")
57
+ table.add_column("ID", style="dim")
58
+ table.add_column("Name", style="cyan")
59
+ table.add_column("External Port")
60
+ table.add_column("Internal IP")
61
+ table.add_column("Internal Port")
62
+ table.add_column("Protocol")
63
+
64
+ for fwd in forwards:
65
+ table.add_row(
66
+ str(fwd.get("id", "")),
67
+ fwd.get("name", ""),
68
+ str(fwd.get("external_port", "")),
69
+ fwd.get("internal_ip", ""),
70
+ str(fwd.get("internal_port", "")),
71
+ fwd.get("protocol", "tcp"),
72
+ )
73
+
74
+ console.print(table)
75
+
76
+ await run_with_client(get_forwards)
77
+
78
+ asyncio.run(run_cmd())
79
+
80
+
81
+ @forwards_group.command(name="show")
82
+ @click.argument("forward_id")
83
+ @click.pass_context
84
+ def forwards_show(ctx: click.Context, forward_id: str) -> None:
85
+ """Show details of a port forward."""
86
+ cli_ctx = get_cli_context(ctx)
87
+ console = cli_ctx.console
88
+ renderer = cli_ctx.renderer
89
+
90
+ async def run_cmd() -> None:
91
+ async def get_forward(client: EeroClient) -> None:
92
+ with cli_ctx.status("Getting port forward..."):
93
+ forwards = await client.get_forwards(cli_ctx.network_id)
94
+
95
+ target = None
96
+ for fwd in forwards:
97
+ if str(fwd.get("id")) == forward_id:
98
+ target = fwd
99
+ break
100
+
101
+ if not target:
102
+ console.print(f"[red]Port forward '{forward_id}' not found[/red]")
103
+ sys.exit(ExitCode.NOT_FOUND)
104
+
105
+ if cli_ctx.is_json_output():
106
+ renderer.render_json(target, "eero.network.forwards.show/v1")
107
+ else:
108
+ content = "\n".join(f"[bold]{k}:[/bold] {v}" for k, v in target.items())
109
+ console.print(
110
+ Panel(content, title=f"Port Forward: {forward_id}", border_style="blue")
111
+ )
112
+
113
+ await run_with_client(get_forward)
114
+
115
+ asyncio.run(run_cmd())
@@ -0,0 +1,162 @@
1
+ """Guest network commands for the Eero CLI.
2
+
3
+ Commands:
4
+ - eero network guest show: Show guest network settings
5
+ - eero network guest enable: Enable guest network
6
+ - eero network guest disable: Disable guest network
7
+ - eero network guest set: Configure guest network
8
+ """
9
+
10
+ import asyncio
11
+ import sys
12
+ from typing import Optional
13
+
14
+ import click
15
+ from eero import EeroClient
16
+ from rich.panel import Panel
17
+
18
+ from ...context import EeroCliContext, get_cli_context
19
+ from ...exit_codes import ExitCode
20
+ from ...safety import OperationRisk, SafetyError, confirm_or_fail
21
+ from ...utils import run_with_client
22
+
23
+
24
+ @click.group(name="guest")
25
+ @click.pass_context
26
+ def guest_group(ctx: click.Context) -> None:
27
+ """Manage guest network.
28
+
29
+ \b
30
+ Commands:
31
+ show - Show guest network settings
32
+ enable - Enable guest network
33
+ disable - Disable guest network
34
+ set - Configure guest network
35
+
36
+ \b
37
+ Examples:
38
+ eero network guest show
39
+ eero network guest enable
40
+ eero network guest set --name "Guest WiFi" --password "secret123"
41
+ """
42
+ pass
43
+
44
+
45
+ @guest_group.command(name="show")
46
+ @click.pass_context
47
+ def guest_show(ctx: click.Context) -> None:
48
+ """Show guest network settings."""
49
+ cli_ctx = get_cli_context(ctx)
50
+ console = cli_ctx.console
51
+ renderer = cli_ctx.renderer
52
+
53
+ async def run_cmd() -> None:
54
+ async def get_guest(client: EeroClient) -> None:
55
+ with cli_ctx.status("Getting guest network settings..."):
56
+ network = await client.get_network(cli_ctx.network_id)
57
+
58
+ if cli_ctx.is_json_output():
59
+ data = {
60
+ "enabled": network.guest_network_enabled,
61
+ "name": network.guest_network_name,
62
+ "password": "********" if network.guest_network_password else None,
63
+ }
64
+ renderer.render_json(data, "eero.network.guest.show/v1")
65
+ else:
66
+ enabled = network.guest_network_enabled
67
+ content = (
68
+ f"[bold]Enabled:[/bold] {'[green]Yes[/green]' if enabled else '[dim]No[/dim]'}\n"
69
+ f"[bold]Name:[/bold] {network.guest_network_name or 'N/A'}\n"
70
+ f"[bold]Password:[/bold] {'********' if network.guest_network_password else 'N/A'}"
71
+ )
72
+ console.print(Panel(content, title="Guest Network", border_style="blue"))
73
+
74
+ await run_with_client(get_guest)
75
+
76
+ asyncio.run(run_cmd())
77
+
78
+
79
+ @guest_group.command(name="enable")
80
+ @click.option("--force", "-f", is_flag=True, help="Skip confirmation")
81
+ @click.pass_context
82
+ def guest_enable(ctx: click.Context, force: bool) -> None:
83
+ """Enable guest network."""
84
+ cli_ctx = get_cli_context(ctx)
85
+ _set_guest_network(cli_ctx, True, None, None, force)
86
+
87
+
88
+ @guest_group.command(name="disable")
89
+ @click.option("--force", "-f", is_flag=True, help="Skip confirmation")
90
+ @click.pass_context
91
+ def guest_disable(ctx: click.Context, force: bool) -> None:
92
+ """Disable guest network."""
93
+ cli_ctx = get_cli_context(ctx)
94
+ _set_guest_network(cli_ctx, False, None, None, force)
95
+
96
+
97
+ @guest_group.command(name="set")
98
+ @click.option("--name", help="Guest network name")
99
+ @click.option("--password", help="Guest network password")
100
+ @click.option("--force", "-f", is_flag=True, help="Skip confirmation")
101
+ @click.pass_context
102
+ def guest_set(
103
+ ctx: click.Context, name: Optional[str], password: Optional[str], force: bool
104
+ ) -> None:
105
+ """Configure guest network settings.
106
+
107
+ \b
108
+ Options:
109
+ --name Guest network SSID
110
+ --password Guest network password
111
+
112
+ \b
113
+ Examples:
114
+ eero network guest set --name "Guest WiFi" --password "welcome123"
115
+ """
116
+ cli_ctx = get_cli_context(ctx)
117
+ _set_guest_network(cli_ctx, True, name, password, force)
118
+
119
+
120
+ def _set_guest_network(
121
+ cli_ctx: EeroCliContext,
122
+ enable: bool,
123
+ name: Optional[str],
124
+ password: Optional[str],
125
+ force: bool,
126
+ ) -> None:
127
+ """Set guest network settings."""
128
+ console = cli_ctx.console
129
+ action = "enable" if enable else "disable"
130
+
131
+ try:
132
+ confirm_or_fail(
133
+ action=f"{action} guest network",
134
+ target="network",
135
+ risk=OperationRisk.MEDIUM,
136
+ force=force or cli_ctx.force,
137
+ non_interactive=cli_ctx.non_interactive,
138
+ dry_run=cli_ctx.dry_run,
139
+ )
140
+ except SafetyError as e:
141
+ cli_ctx.renderer.render_error(e.message)
142
+ sys.exit(e.exit_code)
143
+
144
+ async def run_cmd() -> None:
145
+ async def set_guest(client: EeroClient) -> None:
146
+ with cli_ctx.status(f"{action.capitalize()}ing guest network..."):
147
+ result = await client.set_guest_network(
148
+ enabled=enable,
149
+ name=name,
150
+ password=password,
151
+ network_id=cli_ctx.network_id,
152
+ )
153
+
154
+ if result:
155
+ console.print(f"[bold green]Guest network {action}d[/bold green]")
156
+ else:
157
+ console.print(f"[red]Failed to {action} guest network[/red]")
158
+ sys.exit(ExitCode.GENERIC_ERROR)
159
+
160
+ await run_with_client(set_guest)
161
+
162
+ asyncio.run(run_cmd())