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,224 @@
1
+ """Base Eero commands for the Eero CLI.
2
+
3
+ Commands:
4
+ - eero eero list: List all mesh nodes
5
+ - eero eero show: Show node details
6
+ - eero eero reboot: Reboot a node
7
+ """
8
+
9
+ import asyncio
10
+ import sys
11
+ from typing import Literal, Optional
12
+
13
+ import click
14
+ from eero import EeroClient
15
+ from eero.exceptions import EeroException, EeroNotFoundException
16
+ from rich.table import Table
17
+
18
+ from ...context import ensure_cli_context
19
+ from ...errors import is_not_found_error
20
+ from ...exit_codes import ExitCode
21
+ from ...options import apply_options, force_option, network_option, output_option
22
+ from ...output import OutputFormat
23
+ from ...safety import OperationRisk, SafetyError, confirm_or_fail
24
+ from ...utils import run_with_client
25
+
26
+
27
+ @click.group(name="eero")
28
+ @click.pass_context
29
+ def eero_group(ctx: click.Context) -> None:
30
+ """Manage Eero mesh nodes.
31
+
32
+ \b
33
+ Commands:
34
+ list - List all mesh nodes
35
+ show - Show node details
36
+ reboot - Reboot a node
37
+ led - LED management
38
+ nightlight - Nightlight (Beacon only)
39
+ updates - Update management
40
+
41
+ \b
42
+ Examples:
43
+ eero eero list # List all nodes
44
+ eero eero show "Living Room" # Show node by name
45
+ eero eero reboot "Office" --force # Reboot node
46
+ eero eero led show "Kitchen" # Show LED status
47
+ """
48
+ ensure_cli_context(ctx)
49
+
50
+
51
+ @eero_group.command(name="list")
52
+ @output_option
53
+ @network_option
54
+ @click.pass_context
55
+ def eero_list(ctx: click.Context, output: Optional[str], network_id: Optional[str]) -> None:
56
+ """List all Eero mesh nodes."""
57
+ cli_ctx = apply_options(ctx, output=output, network_id=network_id)
58
+ console = cli_ctx.console
59
+
60
+ async def run_cmd() -> None:
61
+ async def get_eeros(client: EeroClient) -> None:
62
+ with cli_ctx.status("Getting Eero devices..."):
63
+ eeros = await client.get_eeros(cli_ctx.network_id)
64
+
65
+ if not eeros:
66
+ console.print("[yellow]No Eero devices found[/yellow]")
67
+ return
68
+
69
+ if cli_ctx.is_structured_output():
70
+ data = [e.model_dump(mode="json") for e in eeros]
71
+ cli_ctx.render_structured(data, "eero.eero.list/v1")
72
+ elif cli_ctx.output_format == OutputFormat.LIST:
73
+ for e in eeros:
74
+ role = "Gateway" if e.gateway else "Leaf"
75
+ # Use print() with fixed-width columns for alignment
76
+ print(
77
+ f"{e.eero_id or '':<14} {str(e.location) if e.location else '':<20} "
78
+ f"{e.model or '':<15} {e.ip_address or '':<15} {e.status or '':<10} "
79
+ f"{role:<8} {e.connection_type or ''}"
80
+ )
81
+ else:
82
+ table = Table(title="Eero Devices")
83
+ table.add_column("ID", style="dim")
84
+ table.add_column("Name", style="cyan")
85
+ table.add_column("Model", style="green")
86
+ table.add_column("IP", style="blue")
87
+ table.add_column("Status")
88
+ table.add_column("Role")
89
+ table.add_column("Connection", style="magenta")
90
+
91
+ for e in eeros:
92
+ status_color = "green" if e.status == "green" else "red"
93
+ role = "Gateway" if e.gateway else "Leaf"
94
+ table.add_row(
95
+ e.eero_id or "",
96
+ str(e.location) if e.location else "",
97
+ e.model or "",
98
+ e.ip_address or "",
99
+ f"[{status_color}]{e.status}[/{status_color}]",
100
+ role,
101
+ e.connection_type or "",
102
+ )
103
+
104
+ console.print(table)
105
+
106
+ await run_with_client(get_eeros)
107
+
108
+ asyncio.run(run_cmd())
109
+
110
+
111
+ @eero_group.command(name="show")
112
+ @click.argument("eero_id")
113
+ @output_option
114
+ @network_option
115
+ @click.pass_context
116
+ def eero_show(
117
+ ctx: click.Context, eero_id: str, output: Optional[str], network_id: Optional[str]
118
+ ) -> None:
119
+ """Show details of a specific Eero node.
120
+
121
+ \b
122
+ Arguments:
123
+ EERO_ID Node ID, serial, or location name
124
+ """
125
+ cli_ctx = apply_options(ctx, output=output, network_id=network_id)
126
+ console = cli_ctx.console
127
+
128
+ async def run_cmd() -> None:
129
+ async def get_eero(client: EeroClient) -> None:
130
+ with cli_ctx.status(f"Getting Eero '{eero_id}'..."):
131
+ try:
132
+ eero = await client.get_eero(eero_id, cli_ctx.network_id)
133
+ except (EeroNotFoundException, EeroException) as e:
134
+ if is_not_found_error(e):
135
+ console.print(f"[red]Eero '{eero_id}' not found[/red]")
136
+ sys.exit(ExitCode.NOT_FOUND)
137
+ raise
138
+
139
+ if cli_ctx.is_structured_output():
140
+ cli_ctx.render_structured(eero.model_dump(mode="json"), "eero.eero.show/v1")
141
+ else:
142
+ from ...formatting import print_eero_details
143
+
144
+ detail: Literal["brief", "full"] = (
145
+ "full" if cli_ctx.detail_level == "full" else "brief"
146
+ )
147
+ print_eero_details(eero, detail_level=detail)
148
+
149
+ await run_with_client(get_eero)
150
+
151
+ asyncio.run(run_cmd())
152
+
153
+
154
+ @eero_group.command(name="reboot")
155
+ @click.argument("eero_id")
156
+ @force_option
157
+ @network_option
158
+ @click.pass_context
159
+ def eero_reboot(
160
+ ctx: click.Context, eero_id: str, force: Optional[bool], network_id: Optional[str]
161
+ ) -> None:
162
+ """Reboot an Eero node.
163
+
164
+ This is a disruptive operation that will temporarily
165
+ disconnect clients connected to this node.
166
+
167
+ \b
168
+ Arguments:
169
+ EERO_ID Node ID, serial, or location name
170
+ """
171
+ cli_ctx = apply_options(ctx, network_id=network_id, force=force)
172
+ console = cli_ctx.console
173
+
174
+ async def run_cmd() -> None:
175
+ async def reboot_eero(client: EeroClient) -> None:
176
+ # First resolve the eero to get its name
177
+ with cli_ctx.status(f"Finding Eero '{eero_id}'..."):
178
+ try:
179
+ eero = await client.get_eero(eero_id, cli_ctx.network_id)
180
+ except (EeroNotFoundException, EeroException) as e:
181
+ if is_not_found_error(e):
182
+ console.print(f"[red]Eero '{eero_id}' not found[/red]")
183
+ sys.exit(ExitCode.NOT_FOUND)
184
+ raise
185
+
186
+ eero_name = str(eero.location) if eero.location else eero.serial or eero_id
187
+
188
+ try:
189
+ confirm_or_fail(
190
+ action="reboot",
191
+ target=eero_name,
192
+ risk=OperationRisk.MEDIUM,
193
+ force=cli_ctx.force,
194
+ non_interactive=cli_ctx.non_interactive,
195
+ dry_run=cli_ctx.dry_run,
196
+ console=cli_ctx.console,
197
+ )
198
+ except SafetyError as e:
199
+ cli_ctx.renderer.render_error(e.message)
200
+ sys.exit(e.exit_code)
201
+
202
+ with cli_ctx.status(f"Rebooting {eero_name}..."):
203
+ result = await client.reboot_eero(eero.eero_id, cli_ctx.network_id)
204
+
205
+ if result:
206
+ console.print(f"[bold green]Reboot initiated for {eero_name}[/bold green]")
207
+ else:
208
+ console.print(f"[red]Failed to reboot {eero_name}[/red]")
209
+ sys.exit(ExitCode.GENERIC_ERROR)
210
+
211
+ await run_with_client(reboot_eero)
212
+
213
+ asyncio.run(run_cmd())
214
+
215
+
216
+ # Import and register subcommand groups after eero_group is defined
217
+ from .led import led_group # noqa: E402
218
+ from .nightlight import nightlight_group # noqa: E402
219
+ from .updates import updates_group # noqa: E402
220
+
221
+ # Register all subcommand groups
222
+ eero_group.add_command(led_group)
223
+ eero_group.add_command(nightlight_group)
224
+ eero_group.add_command(updates_group)
@@ -0,0 +1,154 @@
1
+ """LED commands for the Eero CLI.
2
+
3
+ Commands:
4
+ - eero eero led show: Show LED status
5
+ - eero eero led on: Turn LED on
6
+ - eero eero led off: Turn LED off
7
+ - eero eero led brightness: Set LED brightness
8
+ """
9
+
10
+ import asyncio
11
+ import sys
12
+
13
+ import click
14
+ from eero import EeroClient
15
+ from eero.exceptions import EeroNotFoundException
16
+ from rich.panel import Panel
17
+
18
+ from ...context import EeroCliContext, get_cli_context
19
+ from ...exit_codes import ExitCode
20
+ from ...utils import run_with_client
21
+
22
+
23
+ @click.group(name="led")
24
+ @click.pass_context
25
+ def led_group(ctx: click.Context) -> None:
26
+ """Manage LED settings.
27
+
28
+ \b
29
+ Commands:
30
+ show - Show LED status
31
+ on - Turn LED on
32
+ off - Turn LED off
33
+ brightness - Set LED brightness
34
+ """
35
+ pass
36
+
37
+
38
+ @led_group.command(name="show")
39
+ @click.argument("eero_id")
40
+ @click.pass_context
41
+ def led_show(ctx: click.Context, eero_id: str) -> None:
42
+ """Show LED status for an Eero node."""
43
+ cli_ctx = get_cli_context(ctx)
44
+ console = cli_ctx.console
45
+ renderer = cli_ctx.renderer
46
+
47
+ async def run_cmd() -> None:
48
+ async def get_led(client: EeroClient) -> None:
49
+ # Resolve eero first (by ID, serial, location, or MAC)
50
+ with cli_ctx.status(f"Finding Eero '{eero_id}'..."):
51
+ try:
52
+ eero = await client.get_eero(eero_id, cli_ctx.network_id)
53
+ except EeroNotFoundException:
54
+ console.print(f"[red]Eero '{eero_id}' not found[/red]")
55
+ sys.exit(ExitCode.NOT_FOUND)
56
+
57
+ with cli_ctx.status("Getting LED status..."):
58
+ led_data = await client.get_led_status(eero.eero_id, cli_ctx.network_id)
59
+
60
+ if cli_ctx.is_json_output():
61
+ renderer.render_json(led_data, "eero.eero.led.show/v1")
62
+ else:
63
+ led_on = led_data.get("led_on", False)
64
+ brightness = led_data.get("led_brightness", 100)
65
+
66
+ content = (
67
+ f"[bold]Status:[/bold] {'[green]On[/green]' if led_on else '[dim]Off[/dim]'}\n"
68
+ f"[bold]Brightness:[/bold] {brightness}%"
69
+ )
70
+ console.print(Panel(content, title="LED Settings", border_style="blue"))
71
+
72
+ await run_with_client(get_led)
73
+
74
+ asyncio.run(run_cmd())
75
+
76
+
77
+ @led_group.command(name="on")
78
+ @click.argument("eero_id")
79
+ @click.pass_context
80
+ def led_on(ctx: click.Context, eero_id: str) -> None:
81
+ """Turn LED on."""
82
+ cli_ctx = get_cli_context(ctx)
83
+ _set_led(cli_ctx, eero_id, True)
84
+
85
+
86
+ @led_group.command(name="off")
87
+ @click.argument("eero_id")
88
+ @click.pass_context
89
+ def led_off(ctx: click.Context, eero_id: str) -> None:
90
+ """Turn LED off."""
91
+ cli_ctx = get_cli_context(ctx)
92
+ _set_led(cli_ctx, eero_id, False)
93
+
94
+
95
+ def _set_led(cli_ctx: EeroCliContext, eero_id: str, enabled: bool) -> None:
96
+ """Set LED state."""
97
+ console = cli_ctx.console
98
+ action = "on" if enabled else "off"
99
+
100
+ async def run_cmd() -> None:
101
+ async def set_led(client: EeroClient) -> None:
102
+ # Resolve eero first (by ID, serial, location, or MAC)
103
+ with cli_ctx.status(f"Finding Eero '{eero_id}'..."):
104
+ try:
105
+ eero = await client.get_eero(eero_id, cli_ctx.network_id)
106
+ except EeroNotFoundException:
107
+ console.print(f"[red]Eero '{eero_id}' not found[/red]")
108
+ sys.exit(ExitCode.NOT_FOUND)
109
+
110
+ with cli_ctx.status(f"Turning LED {action}..."):
111
+ result = await client.set_led(eero.eero_id, enabled, cli_ctx.network_id)
112
+
113
+ if result:
114
+ console.print(f"[bold green]LED turned {action}[/bold green]")
115
+ else:
116
+ console.print(f"[red]Failed to turn LED {action}[/red]")
117
+ sys.exit(ExitCode.GENERIC_ERROR)
118
+
119
+ await run_with_client(set_led)
120
+
121
+ asyncio.run(run_cmd())
122
+
123
+
124
+ @led_group.command(name="brightness")
125
+ @click.argument("eero_id")
126
+ @click.argument("value", type=click.IntRange(0, 100))
127
+ @click.pass_context
128
+ def led_brightness(ctx: click.Context, eero_id: str, value: int) -> None:
129
+ """Set LED brightness (0-100)."""
130
+ cli_ctx = get_cli_context(ctx)
131
+ console = cli_ctx.console
132
+
133
+ async def run_cmd() -> None:
134
+ async def set_brightness(client: EeroClient) -> None:
135
+ # Resolve eero first (by ID, serial, location, or MAC)
136
+ with cli_ctx.status(f"Finding Eero '{eero_id}'..."):
137
+ try:
138
+ eero = await client.get_eero(eero_id, cli_ctx.network_id)
139
+ except EeroNotFoundException:
140
+ console.print(f"[red]Eero '{eero_id}' not found[/red]")
141
+ sys.exit(ExitCode.NOT_FOUND)
142
+
143
+ with cli_ctx.status(f"Setting LED brightness to {value}%..."):
144
+ result = await client.set_led_brightness(eero.eero_id, value, cli_ctx.network_id)
145
+
146
+ if result:
147
+ console.print(f"[bold green]LED brightness set to {value}%[/bold green]")
148
+ else:
149
+ console.print("[red]Failed to set LED brightness[/red]")
150
+ sys.exit(ExitCode.GENERIC_ERROR)
151
+
152
+ await run_with_client(set_brightness)
153
+
154
+ asyncio.run(run_cmd())
@@ -0,0 +1,235 @@
1
+ """Nightlight commands for the Eero CLI (Beacon only).
2
+
3
+ Commands:
4
+ - eero eero nightlight show: Show nightlight settings
5
+ - eero eero nightlight on: Turn nightlight on
6
+ - eero eero nightlight off: Turn nightlight off
7
+ - eero eero nightlight brightness: Set brightness
8
+ - eero eero nightlight schedule: Set schedule
9
+ """
10
+
11
+ import asyncio
12
+ import sys
13
+
14
+ import click
15
+ from eero import EeroClient
16
+ from eero.exceptions import EeroNotFoundException
17
+ from rich.panel import Panel
18
+
19
+ from ...context import EeroCliContext, get_cli_context
20
+ from ...errors import is_feature_unavailable_error
21
+ from ...exit_codes import ExitCode
22
+ from ...utils import run_with_client
23
+
24
+
25
+ @click.group(name="nightlight")
26
+ @click.pass_context
27
+ def nightlight_group(ctx: click.Context) -> None:
28
+ """Manage nightlight (Eero Beacon only).
29
+
30
+ \b
31
+ Commands:
32
+ show - Show nightlight settings
33
+ on - Turn nightlight on
34
+ off - Turn nightlight off
35
+ brightness - Set brightness
36
+ schedule - Set schedule
37
+ """
38
+ pass
39
+
40
+
41
+ @nightlight_group.command(name="show")
42
+ @click.argument("eero_id")
43
+ @click.pass_context
44
+ def nightlight_show(ctx: click.Context, eero_id: str) -> None:
45
+ """Show nightlight 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_nightlight(client: EeroClient) -> None:
52
+ # Resolve eero first (by ID, serial, location, or MAC)
53
+ with cli_ctx.status(f"Finding Eero '{eero_id}'..."):
54
+ try:
55
+ eero = await client.get_eero(eero_id, cli_ctx.network_id)
56
+ except EeroNotFoundException:
57
+ console.print(f"[red]Eero '{eero_id}' not found[/red]")
58
+ sys.exit(ExitCode.NOT_FOUND)
59
+
60
+ with cli_ctx.status("Getting nightlight settings..."):
61
+ try:
62
+ nl_data = await client.get_nightlight(eero.eero_id, cli_ctx.network_id)
63
+ except Exception as e:
64
+ if is_feature_unavailable_error(e, "beacon"):
65
+ console.print(
66
+ "[yellow]Nightlight is only available on Eero Beacon devices[/yellow]"
67
+ )
68
+ sys.exit(ExitCode.FEATURE_UNAVAILABLE)
69
+ raise
70
+
71
+ if cli_ctx.is_json_output():
72
+ renderer.render_json(nl_data, "eero.eero.nightlight.show/v1")
73
+ else:
74
+ enabled = nl_data.get("enabled", False)
75
+ brightness = nl_data.get("brightness", 100)
76
+ schedule_enabled = nl_data.get("schedule_enabled", False)
77
+
78
+ content = (
79
+ f"[bold]Enabled:[/bold] {'[green]Yes[/green]' if enabled else '[dim]No[/dim]'}\n"
80
+ f"[bold]Brightness:[/bold] {brightness}%"
81
+ )
82
+ if schedule_enabled:
83
+ on_time = nl_data.get("on_time", "N/A")
84
+ off_time = nl_data.get("off_time", "N/A")
85
+ content += f"\n[bold]Schedule:[/bold] {on_time} - {off_time}"
86
+
87
+ console.print(Panel(content, title="Nightlight Settings", border_style="blue"))
88
+
89
+ await run_with_client(get_nightlight)
90
+
91
+ asyncio.run(run_cmd())
92
+
93
+
94
+ @nightlight_group.command(name="on")
95
+ @click.argument("eero_id")
96
+ @click.pass_context
97
+ def nightlight_on(ctx: click.Context, eero_id: str) -> None:
98
+ """Turn nightlight on."""
99
+ cli_ctx = get_cli_context(ctx)
100
+ _set_nightlight(cli_ctx, eero_id, True)
101
+
102
+
103
+ @nightlight_group.command(name="off")
104
+ @click.argument("eero_id")
105
+ @click.pass_context
106
+ def nightlight_off(ctx: click.Context, eero_id: str) -> None:
107
+ """Turn nightlight off."""
108
+ cli_ctx = get_cli_context(ctx)
109
+ _set_nightlight(cli_ctx, eero_id, False)
110
+
111
+
112
+ def _set_nightlight(cli_ctx: EeroCliContext, eero_id: str, enabled: bool) -> None:
113
+ """Set nightlight state."""
114
+ console = cli_ctx.console
115
+ action = "on" if enabled else "off"
116
+
117
+ async def run_cmd() -> None:
118
+ async def set_nl(client: EeroClient) -> None:
119
+ # Resolve eero first (by ID, serial, location, or MAC)
120
+ with cli_ctx.status(f"Finding Eero '{eero_id}'..."):
121
+ try:
122
+ eero = await client.get_eero(eero_id, cli_ctx.network_id)
123
+ except EeroNotFoundException:
124
+ console.print(f"[red]Eero '{eero_id}' not found[/red]")
125
+ sys.exit(ExitCode.NOT_FOUND)
126
+
127
+ with cli_ctx.status(f"Turning nightlight {action}..."):
128
+ try:
129
+ result = await client.set_nightlight(
130
+ eero.eero_id, enabled=enabled, network_id=cli_ctx.network_id
131
+ )
132
+ except Exception as e:
133
+ if is_feature_unavailable_error(e, "beacon"):
134
+ console.print(
135
+ "[yellow]Nightlight is only available on Eero Beacon devices[/yellow]"
136
+ )
137
+ sys.exit(ExitCode.FEATURE_UNAVAILABLE)
138
+ raise
139
+
140
+ if result:
141
+ console.print(f"[bold green]Nightlight turned {action}[/bold green]")
142
+ else:
143
+ console.print(f"[red]Failed to turn nightlight {action}[/red]")
144
+ sys.exit(ExitCode.GENERIC_ERROR)
145
+
146
+ await run_with_client(set_nl)
147
+
148
+ asyncio.run(run_cmd())
149
+
150
+
151
+ @nightlight_group.command(name="brightness")
152
+ @click.argument("eero_id")
153
+ @click.argument("value", type=click.IntRange(0, 100))
154
+ @click.pass_context
155
+ def nightlight_brightness(ctx: click.Context, eero_id: str, value: int) -> None:
156
+ """Set nightlight brightness (0-100)."""
157
+ cli_ctx = get_cli_context(ctx)
158
+ console = cli_ctx.console
159
+
160
+ async def run_cmd() -> None:
161
+ async def set_brightness(client: EeroClient) -> None:
162
+ # Resolve eero first (by ID, serial, location, or MAC)
163
+ with cli_ctx.status(f"Finding Eero '{eero_id}'..."):
164
+ try:
165
+ eero = await client.get_eero(eero_id, cli_ctx.network_id)
166
+ except EeroNotFoundException:
167
+ console.print(f"[red]Eero '{eero_id}' not found[/red]")
168
+ sys.exit(ExitCode.NOT_FOUND)
169
+
170
+ with cli_ctx.status(f"Setting nightlight brightness to {value}%..."):
171
+ try:
172
+ result = await client.set_nightlight_brightness(
173
+ eero.eero_id, value, cli_ctx.network_id
174
+ )
175
+ except Exception as e:
176
+ if is_feature_unavailable_error(e, "beacon"):
177
+ console.print(
178
+ "[yellow]Nightlight is only available on Eero Beacon devices[/yellow]"
179
+ )
180
+ sys.exit(ExitCode.FEATURE_UNAVAILABLE)
181
+ raise
182
+
183
+ if result:
184
+ console.print(f"[bold green]Nightlight brightness set to {value}%[/bold green]")
185
+ else:
186
+ console.print("[red]Failed to set nightlight brightness[/red]")
187
+ sys.exit(ExitCode.GENERIC_ERROR)
188
+
189
+ await run_with_client(set_brightness)
190
+
191
+ asyncio.run(run_cmd())
192
+
193
+
194
+ @nightlight_group.command(name="schedule")
195
+ @click.argument("eero_id")
196
+ @click.option("--on-time", required=True, help="Time to turn on (HH:MM)")
197
+ @click.option("--off-time", required=True, help="Time to turn off (HH:MM)")
198
+ @click.pass_context
199
+ def nightlight_schedule(ctx: click.Context, eero_id: str, on_time: str, off_time: str) -> None:
200
+ """Set nightlight schedule."""
201
+ cli_ctx = get_cli_context(ctx)
202
+ console = cli_ctx.console
203
+
204
+ async def run_cmd() -> None:
205
+ async def set_schedule(client: EeroClient) -> None:
206
+ # Resolve eero first (by ID, serial, location, or MAC)
207
+ with cli_ctx.status(f"Finding Eero '{eero_id}'..."):
208
+ try:
209
+ eero = await client.get_eero(eero_id, cli_ctx.network_id)
210
+ except EeroNotFoundException:
211
+ console.print(f"[red]Eero '{eero_id}' not found[/red]")
212
+ sys.exit(ExitCode.NOT_FOUND)
213
+
214
+ with cli_ctx.status("Setting nightlight schedule..."):
215
+ try:
216
+ result = await client.set_nightlight_schedule(
217
+ eero.eero_id, True, on_time, off_time, cli_ctx.network_id
218
+ )
219
+ except Exception as e:
220
+ if is_feature_unavailable_error(e, "beacon"):
221
+ console.print(
222
+ "[yellow]Nightlight is only available on Eero Beacon devices[/yellow]"
223
+ )
224
+ sys.exit(ExitCode.FEATURE_UNAVAILABLE)
225
+ raise
226
+
227
+ if result:
228
+ console.print(f"[bold green]Schedule set: {on_time} - {off_time}[/bold green]")
229
+ else:
230
+ console.print("[red]Failed to set schedule[/red]")
231
+ sys.exit(ExitCode.GENERIC_ERROR)
232
+
233
+ await run_with_client(set_schedule)
234
+
235
+ asyncio.run(run_cmd())
@@ -0,0 +1,82 @@
1
+ """Update commands for the Eero CLI.
2
+
3
+ Commands:
4
+ - eero eero updates show: Show update status
5
+ - eero eero updates check: Check for updates
6
+ """
7
+
8
+ import asyncio
9
+
10
+ import click
11
+ from eero import EeroClient
12
+ from rich.panel import Panel
13
+
14
+ from ...context import get_cli_context
15
+ from ...utils import run_with_client
16
+
17
+
18
+ @click.group(name="updates")
19
+ @click.pass_context
20
+ def updates_group(ctx: click.Context) -> None:
21
+ """Manage updates.
22
+
23
+ \b
24
+ Commands:
25
+ show - Show update status
26
+ check - Check for updates
27
+ """
28
+ pass
29
+
30
+
31
+ @updates_group.command(name="show")
32
+ @click.pass_context
33
+ def updates_show(ctx: click.Context) -> None:
34
+ """Show update status."""
35
+ cli_ctx = get_cli_context(ctx)
36
+ console = cli_ctx.console
37
+ renderer = cli_ctx.renderer
38
+
39
+ async def run_cmd() -> None:
40
+ async def get_updates(client: EeroClient) -> None:
41
+ with cli_ctx.status("Getting update status..."):
42
+ updates = await client.get_updates(cli_ctx.network_id)
43
+
44
+ if cli_ctx.is_json_output():
45
+ renderer.render_json(updates, "eero.eero.updates.show/v1")
46
+ else:
47
+ has_update = updates.get("has_update", False)
48
+ target = updates.get("target_firmware", "N/A")
49
+
50
+ content = (
51
+ f"[bold]Update Available:[/bold] {'[green]Yes[/green]' if has_update else '[dim]No[/dim]'}\n"
52
+ f"[bold]Target Firmware:[/bold] {target}"
53
+ )
54
+ console.print(Panel(content, title="Update Status", border_style="blue"))
55
+
56
+ await run_with_client(get_updates)
57
+
58
+ asyncio.run(run_cmd())
59
+
60
+
61
+ @updates_group.command(name="check")
62
+ @click.pass_context
63
+ def updates_check(ctx: click.Context) -> None:
64
+ """Check for available updates."""
65
+ cli_ctx = get_cli_context(ctx)
66
+ console = cli_ctx.console
67
+
68
+ async def run_cmd() -> None:
69
+ async def check_updates(client: EeroClient) -> None:
70
+ with cli_ctx.status("Checking for updates..."):
71
+ updates = await client.get_updates(cli_ctx.network_id)
72
+
73
+ has_update = updates.get("has_update", False)
74
+ if has_update:
75
+ target = updates.get("target_firmware", "N/A")
76
+ console.print(f"[bold green]Update available: {target}[/bold green]")
77
+ else:
78
+ console.print("[dim]No updates available[/dim]")
79
+
80
+ await run_with_client(check_updates)
81
+
82
+ asyncio.run(run_cmd())