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