tescmd 0.1.2__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.
- tescmd/__init__.py +3 -0
- tescmd/__main__.py +5 -0
- tescmd/_internal/__init__.py +0 -0
- tescmd/_internal/async_utils.py +25 -0
- tescmd/_internal/permissions.py +43 -0
- tescmd/_internal/vin.py +44 -0
- tescmd/api/__init__.py +1 -0
- tescmd/api/charging.py +102 -0
- tescmd/api/client.py +189 -0
- tescmd/api/command.py +540 -0
- tescmd/api/energy.py +146 -0
- tescmd/api/errors.py +76 -0
- tescmd/api/partner.py +40 -0
- tescmd/api/sharing.py +65 -0
- tescmd/api/signed_command.py +277 -0
- tescmd/api/user.py +38 -0
- tescmd/api/vehicle.py +150 -0
- tescmd/auth/__init__.py +1 -0
- tescmd/auth/oauth.py +312 -0
- tescmd/auth/server.py +108 -0
- tescmd/auth/token_store.py +273 -0
- tescmd/ble/__init__.py +0 -0
- tescmd/cache/__init__.py +6 -0
- tescmd/cache/keys.py +51 -0
- tescmd/cache/response_cache.py +213 -0
- tescmd/cli/__init__.py +0 -0
- tescmd/cli/_client.py +603 -0
- tescmd/cli/_options.py +126 -0
- tescmd/cli/auth.py +682 -0
- tescmd/cli/billing.py +240 -0
- tescmd/cli/cache.py +85 -0
- tescmd/cli/charge.py +610 -0
- tescmd/cli/climate.py +501 -0
- tescmd/cli/energy.py +385 -0
- tescmd/cli/key.py +611 -0
- tescmd/cli/main.py +601 -0
- tescmd/cli/media.py +146 -0
- tescmd/cli/nav.py +242 -0
- tescmd/cli/partner.py +112 -0
- tescmd/cli/raw.py +75 -0
- tescmd/cli/security.py +495 -0
- tescmd/cli/setup.py +786 -0
- tescmd/cli/sharing.py +188 -0
- tescmd/cli/software.py +81 -0
- tescmd/cli/status.py +106 -0
- tescmd/cli/trunk.py +240 -0
- tescmd/cli/user.py +145 -0
- tescmd/cli/vehicle.py +837 -0
- tescmd/config/__init__.py +0 -0
- tescmd/crypto/__init__.py +19 -0
- tescmd/crypto/ecdh.py +46 -0
- tescmd/crypto/keys.py +122 -0
- tescmd/deploy/__init__.py +0 -0
- tescmd/deploy/github_pages.py +268 -0
- tescmd/models/__init__.py +85 -0
- tescmd/models/auth.py +108 -0
- tescmd/models/command.py +18 -0
- tescmd/models/config.py +63 -0
- tescmd/models/energy.py +56 -0
- tescmd/models/sharing.py +26 -0
- tescmd/models/user.py +37 -0
- tescmd/models/vehicle.py +185 -0
- tescmd/output/__init__.py +5 -0
- tescmd/output/formatter.py +132 -0
- tescmd/output/json_output.py +83 -0
- tescmd/output/rich_output.py +809 -0
- tescmd/protocol/__init__.py +23 -0
- tescmd/protocol/commands.py +175 -0
- tescmd/protocol/encoder.py +122 -0
- tescmd/protocol/metadata.py +116 -0
- tescmd/protocol/payloads.py +621 -0
- tescmd/protocol/protobuf/__init__.py +6 -0
- tescmd/protocol/protobuf/messages.py +564 -0
- tescmd/protocol/session.py +318 -0
- tescmd/protocol/signer.py +84 -0
- tescmd/py.typed +0 -0
- tescmd-0.1.2.dist-info/METADATA +458 -0
- tescmd-0.1.2.dist-info/RECORD +81 -0
- tescmd-0.1.2.dist-info/WHEEL +4 -0
- tescmd-0.1.2.dist-info/entry_points.txt +2 -0
- tescmd-0.1.2.dist-info/licenses/LICENSE +21 -0
tescmd/cli/security.py
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
"""CLI commands for security operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from tescmd._internal.async_utils import run_async
|
|
10
|
+
from tescmd.api.errors import ConfigError
|
|
11
|
+
from tescmd.cli._client import (
|
|
12
|
+
auto_wake,
|
|
13
|
+
cached_vehicle_data,
|
|
14
|
+
execute_command,
|
|
15
|
+
get_command_api,
|
|
16
|
+
get_vehicle_api,
|
|
17
|
+
invalidate_cache_for_vin,
|
|
18
|
+
require_vin,
|
|
19
|
+
)
|
|
20
|
+
from tescmd.cli._options import global_options
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from tescmd.cli.main import AppContext
|
|
24
|
+
|
|
25
|
+
security_group = click.Group("security", help="Security and access commands")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Status (read via VehicleAPI)
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@security_group.command("status")
|
|
34
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
35
|
+
@global_options
|
|
36
|
+
def status_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
37
|
+
"""Show current security status (locks, sentry, etc.)."""
|
|
38
|
+
run_async(_cmd_status(app_ctx, vin_positional))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def _cmd_status(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
42
|
+
formatter = app_ctx.formatter
|
|
43
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
44
|
+
client, api = get_vehicle_api(app_ctx)
|
|
45
|
+
try:
|
|
46
|
+
vdata = await cached_vehicle_data(app_ctx, api, vin, endpoints=["vehicle_state"])
|
|
47
|
+
finally:
|
|
48
|
+
await client.close()
|
|
49
|
+
|
|
50
|
+
if formatter.format == "json":
|
|
51
|
+
vs = vdata.vehicle_state
|
|
52
|
+
formatter.output(
|
|
53
|
+
vs.model_dump(exclude_none=True) if vs else {},
|
|
54
|
+
command="security.status",
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
if vdata.vehicle_state:
|
|
58
|
+
formatter.rich.vehicle_status(vdata.vehicle_state)
|
|
59
|
+
else:
|
|
60
|
+
formatter.rich.info("No vehicle state data available.")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Simple commands
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@security_group.command("lock")
|
|
69
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
70
|
+
@global_options
|
|
71
|
+
def lock_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
72
|
+
"""Lock all doors."""
|
|
73
|
+
run_async(
|
|
74
|
+
execute_command(
|
|
75
|
+
app_ctx,
|
|
76
|
+
vin_positional,
|
|
77
|
+
"door_lock",
|
|
78
|
+
"security.lock",
|
|
79
|
+
success_message="Doors locked.",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@security_group.command("auto-secure")
|
|
85
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
86
|
+
@global_options
|
|
87
|
+
def auto_secure_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
88
|
+
"""Close falcon-wing doors and lock (Model X only)."""
|
|
89
|
+
run_async(
|
|
90
|
+
execute_command(
|
|
91
|
+
app_ctx,
|
|
92
|
+
vin_positional,
|
|
93
|
+
"auto_secure_vehicle",
|
|
94
|
+
"security.auto-secure",
|
|
95
|
+
success_message="Vehicle secured (falcon-wing doors closed and locked).",
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@security_group.command("unlock")
|
|
101
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
102
|
+
@global_options
|
|
103
|
+
def unlock_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
104
|
+
"""Unlock all doors."""
|
|
105
|
+
run_async(
|
|
106
|
+
execute_command(
|
|
107
|
+
app_ctx,
|
|
108
|
+
vin_positional,
|
|
109
|
+
"door_unlock",
|
|
110
|
+
"security.unlock",
|
|
111
|
+
success_message="Doors unlocked.",
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@security_group.command("valet-reset")
|
|
117
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
118
|
+
@global_options
|
|
119
|
+
def valet_reset_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
120
|
+
"""Reset valet PIN."""
|
|
121
|
+
run_async(
|
|
122
|
+
execute_command(
|
|
123
|
+
app_ctx,
|
|
124
|
+
vin_positional,
|
|
125
|
+
"reset_valet_pin",
|
|
126
|
+
"security.valet-reset",
|
|
127
|
+
success_message="Valet PIN reset.",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@security_group.command("remote-start")
|
|
133
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
134
|
+
@global_options
|
|
135
|
+
def remote_start_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
136
|
+
"""Enable remote start."""
|
|
137
|
+
run_async(
|
|
138
|
+
execute_command(
|
|
139
|
+
app_ctx,
|
|
140
|
+
vin_positional,
|
|
141
|
+
"remote_start_drive",
|
|
142
|
+
"security.remote-start",
|
|
143
|
+
success_message="Remote start enabled.",
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@security_group.command("flash")
|
|
149
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
150
|
+
@global_options
|
|
151
|
+
def flash_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
152
|
+
"""Flash the vehicle lights."""
|
|
153
|
+
run_async(
|
|
154
|
+
execute_command(
|
|
155
|
+
app_ctx,
|
|
156
|
+
vin_positional,
|
|
157
|
+
"flash_lights",
|
|
158
|
+
"security.flash",
|
|
159
|
+
success_message="Lights flashed.",
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@security_group.command("honk")
|
|
165
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
166
|
+
@global_options
|
|
167
|
+
def honk_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
168
|
+
"""Honk the horn."""
|
|
169
|
+
run_async(
|
|
170
|
+
execute_command(
|
|
171
|
+
app_ctx,
|
|
172
|
+
vin_positional,
|
|
173
|
+
"honk_horn",
|
|
174
|
+
"security.honk",
|
|
175
|
+
success_message="Horn honked.",
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# Parameterised commands
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@security_group.command("sentry")
|
|
186
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
187
|
+
@click.option("--on/--off", default=True, help="Enable or disable sentry mode")
|
|
188
|
+
@global_options
|
|
189
|
+
def sentry_cmd(app_ctx: AppContext, vin_positional: str | None, on: bool) -> None:
|
|
190
|
+
"""Enable or disable sentry mode."""
|
|
191
|
+
run_async(_cmd_sentry(app_ctx, vin_positional, on))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def _cmd_sentry(app_ctx: AppContext, vin_positional: str | None, on: bool) -> None:
|
|
195
|
+
formatter = app_ctx.formatter
|
|
196
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
197
|
+
client, vehicle_api, cmd_api = get_command_api(app_ctx)
|
|
198
|
+
try:
|
|
199
|
+
result = await auto_wake(
|
|
200
|
+
formatter,
|
|
201
|
+
vehicle_api,
|
|
202
|
+
vin,
|
|
203
|
+
lambda: cmd_api.set_sentry_mode(vin, on=on),
|
|
204
|
+
auto=app_ctx.auto_wake,
|
|
205
|
+
)
|
|
206
|
+
finally:
|
|
207
|
+
await client.close()
|
|
208
|
+
|
|
209
|
+
invalidate_cache_for_vin(app_ctx, vin)
|
|
210
|
+
|
|
211
|
+
if formatter.format == "json":
|
|
212
|
+
formatter.output(result, command="security.sentry")
|
|
213
|
+
else:
|
|
214
|
+
state = "enabled" if on else "disabled"
|
|
215
|
+
msg = result.response.reason or f"Sentry mode {state}."
|
|
216
|
+
formatter.rich.command_result(result.response.result, msg)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@security_group.command("valet")
|
|
220
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
221
|
+
@click.option("--on/--off", default=True, help="Enable or disable valet mode")
|
|
222
|
+
@click.option("--password", default=None, help="Valet PIN")
|
|
223
|
+
@global_options
|
|
224
|
+
def valet_cmd(
|
|
225
|
+
app_ctx: AppContext,
|
|
226
|
+
vin_positional: str | None,
|
|
227
|
+
on: bool,
|
|
228
|
+
password: str | None,
|
|
229
|
+
) -> None:
|
|
230
|
+
"""Enable or disable valet mode."""
|
|
231
|
+
run_async(_cmd_valet(app_ctx, vin_positional, on, password))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
async def _cmd_valet(
|
|
235
|
+
app_ctx: AppContext,
|
|
236
|
+
vin_positional: str | None,
|
|
237
|
+
on: bool,
|
|
238
|
+
password: str | None,
|
|
239
|
+
) -> None:
|
|
240
|
+
formatter = app_ctx.formatter
|
|
241
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
242
|
+
client, vehicle_api, cmd_api = get_command_api(app_ctx)
|
|
243
|
+
try:
|
|
244
|
+
result = await auto_wake(
|
|
245
|
+
formatter,
|
|
246
|
+
vehicle_api,
|
|
247
|
+
vin,
|
|
248
|
+
lambda: cmd_api.set_valet_mode(vin, on=on, password=password),
|
|
249
|
+
auto=app_ctx.auto_wake,
|
|
250
|
+
)
|
|
251
|
+
finally:
|
|
252
|
+
await client.close()
|
|
253
|
+
|
|
254
|
+
invalidate_cache_for_vin(app_ctx, vin)
|
|
255
|
+
|
|
256
|
+
if formatter.format == "json":
|
|
257
|
+
formatter.output(result, command="security.valet")
|
|
258
|
+
else:
|
|
259
|
+
state = "enabled" if on else "disabled"
|
|
260
|
+
msg = result.response.reason or f"Valet mode {state}."
|
|
261
|
+
formatter.rich.command_result(result.response.result, msg)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@security_group.command("pin-to-drive")
|
|
265
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
266
|
+
@click.option("--on/--off", default=True, help="Enable or disable PIN to Drive")
|
|
267
|
+
@click.option("--password", default=None, help="PIN code")
|
|
268
|
+
@global_options
|
|
269
|
+
def pin_to_drive_cmd(
|
|
270
|
+
app_ctx: AppContext, vin_positional: str | None, on: bool, password: str | None
|
|
271
|
+
) -> None:
|
|
272
|
+
"""Enable or disable PIN to Drive."""
|
|
273
|
+
body: dict[str, object] = {"on": on}
|
|
274
|
+
if password is not None:
|
|
275
|
+
body["password"] = password
|
|
276
|
+
state = "enabled" if on else "disabled"
|
|
277
|
+
run_async(
|
|
278
|
+
execute_command(
|
|
279
|
+
app_ctx,
|
|
280
|
+
vin_positional,
|
|
281
|
+
"set_pin_to_drive",
|
|
282
|
+
"security.pin-to-drive",
|
|
283
|
+
body=body,
|
|
284
|
+
success_message=f"PIN to Drive {state}.",
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@security_group.command("guest-mode")
|
|
290
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
291
|
+
@click.option("--on/--off", "enable", default=True, help="Enable or disable guest mode")
|
|
292
|
+
@global_options
|
|
293
|
+
def guest_mode_cmd(app_ctx: AppContext, vin_positional: str | None, enable: bool) -> None:
|
|
294
|
+
"""Enable or disable guest mode."""
|
|
295
|
+
state = "enabled" if enable else "disabled"
|
|
296
|
+
run_async(
|
|
297
|
+
execute_command(
|
|
298
|
+
app_ctx,
|
|
299
|
+
vin_positional,
|
|
300
|
+
"guest_mode",
|
|
301
|
+
"security.guest-mode",
|
|
302
|
+
body={"enable": enable},
|
|
303
|
+
success_message=f"Guest mode {state}.",
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@security_group.command("erase-data")
|
|
309
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
310
|
+
@click.option(
|
|
311
|
+
"--confirm",
|
|
312
|
+
is_flag=True,
|
|
313
|
+
required=True,
|
|
314
|
+
help="Required flag to confirm data erasure (DESTRUCTIVE)",
|
|
315
|
+
)
|
|
316
|
+
@global_options
|
|
317
|
+
def erase_data_cmd(app_ctx: AppContext, vin_positional: str | None, confirm: bool) -> None:
|
|
318
|
+
"""Erase all user data from the vehicle (DESTRUCTIVE).
|
|
319
|
+
|
|
320
|
+
Requires --confirm flag.
|
|
321
|
+
"""
|
|
322
|
+
run_async(
|
|
323
|
+
execute_command(
|
|
324
|
+
app_ctx,
|
|
325
|
+
vin_positional,
|
|
326
|
+
"erase_user_data",
|
|
327
|
+
"security.erase-data",
|
|
328
|
+
success_message="User data erased.",
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@security_group.command("boombox")
|
|
334
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
335
|
+
@click.option(
|
|
336
|
+
"--sound",
|
|
337
|
+
type=click.Choice(["locate", "fart"]),
|
|
338
|
+
default="locate",
|
|
339
|
+
help="Sound to play (locate=ping, fart=random fart)",
|
|
340
|
+
)
|
|
341
|
+
@global_options
|
|
342
|
+
def boombox_cmd(app_ctx: AppContext, vin_positional: str | None, sound: str) -> None:
|
|
343
|
+
"""Activate the boombox (external speaker)."""
|
|
344
|
+
sound_id = 0 if sound == "fart" else 2000
|
|
345
|
+
run_async(
|
|
346
|
+
execute_command(
|
|
347
|
+
app_ctx,
|
|
348
|
+
vin_positional,
|
|
349
|
+
"remote_boombox",
|
|
350
|
+
"security.boombox",
|
|
351
|
+
body={"sound": sound_id},
|
|
352
|
+
success_message="Boombox activated.",
|
|
353
|
+
)
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@security_group.command("pin-reset")
|
|
358
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
359
|
+
@global_options
|
|
360
|
+
def pin_reset_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
361
|
+
"""Reset PIN to Drive."""
|
|
362
|
+
run_async(
|
|
363
|
+
execute_command(
|
|
364
|
+
app_ctx,
|
|
365
|
+
vin_positional,
|
|
366
|
+
"reset_pin_to_drive_pin",
|
|
367
|
+
"security.pin-reset",
|
|
368
|
+
success_message="PIN to Drive reset.",
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@security_group.command("pin-clear-admin")
|
|
374
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
375
|
+
@global_options
|
|
376
|
+
def pin_clear_admin_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
377
|
+
"""Admin clear PIN to Drive (fleet manager only)."""
|
|
378
|
+
run_async(
|
|
379
|
+
execute_command(
|
|
380
|
+
app_ctx,
|
|
381
|
+
vin_positional,
|
|
382
|
+
"clear_pin_to_drive_admin",
|
|
383
|
+
"security.pin-clear-admin",
|
|
384
|
+
success_message="PIN cleared (admin).",
|
|
385
|
+
)
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@security_group.command("speed-clear")
|
|
390
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
391
|
+
@click.option("--pin", required=True, help="Speed limit PIN")
|
|
392
|
+
@global_options
|
|
393
|
+
def speed_clear_cmd(app_ctx: AppContext, vin_positional: str | None, pin: str) -> None:
|
|
394
|
+
"""Clear speed limit PIN."""
|
|
395
|
+
run_async(
|
|
396
|
+
execute_command(
|
|
397
|
+
app_ctx,
|
|
398
|
+
vin_positional,
|
|
399
|
+
"speed_limit_clear_pin",
|
|
400
|
+
"security.speed-clear",
|
|
401
|
+
body={"pin": pin},
|
|
402
|
+
success_message="Speed limit PIN cleared.",
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@security_group.command("speed-clear-admin")
|
|
408
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
409
|
+
@global_options
|
|
410
|
+
def speed_clear_admin_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
411
|
+
"""Admin clear speed limit PIN (fleet manager only)."""
|
|
412
|
+
run_async(
|
|
413
|
+
execute_command(
|
|
414
|
+
app_ctx,
|
|
415
|
+
vin_positional,
|
|
416
|
+
"speed_limit_clear_pin_admin",
|
|
417
|
+
"security.speed-clear-admin",
|
|
418
|
+
success_message="Speed limit PIN cleared (admin).",
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@security_group.command("speed-limit")
|
|
424
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
425
|
+
@click.option("--activate", "pin_activate", default=None, help="Activate speed limit with PIN")
|
|
426
|
+
@click.option(
|
|
427
|
+
"--deactivate", "pin_deactivate", default=None, help="Deactivate speed limit with PIN"
|
|
428
|
+
)
|
|
429
|
+
@click.option("--set", "limit_mph", type=float, default=None, help="Set speed limit in MPH")
|
|
430
|
+
@global_options
|
|
431
|
+
def speed_limit_cmd(
|
|
432
|
+
app_ctx: AppContext,
|
|
433
|
+
vin_positional: str | None,
|
|
434
|
+
pin_activate: str | None,
|
|
435
|
+
pin_deactivate: str | None,
|
|
436
|
+
limit_mph: float | None,
|
|
437
|
+
) -> None:
|
|
438
|
+
"""Manage speed limit mode (--activate PIN, --deactivate PIN, or --set MPH)."""
|
|
439
|
+
run_async(_cmd_speed_limit(app_ctx, vin_positional, pin_activate, pin_deactivate, limit_mph))
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
async def _cmd_speed_limit(
|
|
443
|
+
app_ctx: AppContext,
|
|
444
|
+
vin_positional: str | None,
|
|
445
|
+
pin_activate: str | None,
|
|
446
|
+
pin_deactivate: str | None,
|
|
447
|
+
limit_mph: float | None,
|
|
448
|
+
) -> None:
|
|
449
|
+
formatter = app_ctx.formatter
|
|
450
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
451
|
+
|
|
452
|
+
provided = sum(x is not None for x in (pin_activate, pin_deactivate, limit_mph))
|
|
453
|
+
if provided != 1:
|
|
454
|
+
raise ConfigError("Specify exactly one of --activate PIN, --deactivate PIN, or --set MPH.")
|
|
455
|
+
|
|
456
|
+
client, vehicle_api, cmd_api = get_command_api(app_ctx)
|
|
457
|
+
try:
|
|
458
|
+
if pin_activate is not None:
|
|
459
|
+
result = await auto_wake(
|
|
460
|
+
formatter,
|
|
461
|
+
vehicle_api,
|
|
462
|
+
vin,
|
|
463
|
+
lambda: cmd_api.speed_limit_activate(vin, pin=pin_activate),
|
|
464
|
+
auto=app_ctx.auto_wake,
|
|
465
|
+
)
|
|
466
|
+
cmd_name = "security.speed-limit.activate"
|
|
467
|
+
elif pin_deactivate is not None:
|
|
468
|
+
result = await auto_wake(
|
|
469
|
+
formatter,
|
|
470
|
+
vehicle_api,
|
|
471
|
+
vin,
|
|
472
|
+
lambda: cmd_api.speed_limit_deactivate(vin, pin=pin_deactivate),
|
|
473
|
+
auto=app_ctx.auto_wake,
|
|
474
|
+
)
|
|
475
|
+
cmd_name = "security.speed-limit.deactivate"
|
|
476
|
+
else:
|
|
477
|
+
assert limit_mph is not None
|
|
478
|
+
result = await auto_wake(
|
|
479
|
+
formatter,
|
|
480
|
+
vehicle_api,
|
|
481
|
+
vin,
|
|
482
|
+
lambda: cmd_api.speed_limit_set_limit(vin, limit_mph=limit_mph),
|
|
483
|
+
auto=app_ctx.auto_wake,
|
|
484
|
+
)
|
|
485
|
+
cmd_name = "security.speed-limit.set"
|
|
486
|
+
finally:
|
|
487
|
+
await client.close()
|
|
488
|
+
|
|
489
|
+
invalidate_cache_for_vin(app_ctx, vin)
|
|
490
|
+
|
|
491
|
+
if formatter.format == "json":
|
|
492
|
+
formatter.output(result, command=cmd_name)
|
|
493
|
+
else:
|
|
494
|
+
msg = result.response.reason or "Speed limit updated."
|
|
495
|
+
formatter.rich.command_result(result.response.result, msg)
|