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.
Files changed (81) hide show
  1. tescmd/__init__.py +3 -0
  2. tescmd/__main__.py +5 -0
  3. tescmd/_internal/__init__.py +0 -0
  4. tescmd/_internal/async_utils.py +25 -0
  5. tescmd/_internal/permissions.py +43 -0
  6. tescmd/_internal/vin.py +44 -0
  7. tescmd/api/__init__.py +1 -0
  8. tescmd/api/charging.py +102 -0
  9. tescmd/api/client.py +189 -0
  10. tescmd/api/command.py +540 -0
  11. tescmd/api/energy.py +146 -0
  12. tescmd/api/errors.py +76 -0
  13. tescmd/api/partner.py +40 -0
  14. tescmd/api/sharing.py +65 -0
  15. tescmd/api/signed_command.py +277 -0
  16. tescmd/api/user.py +38 -0
  17. tescmd/api/vehicle.py +150 -0
  18. tescmd/auth/__init__.py +1 -0
  19. tescmd/auth/oauth.py +312 -0
  20. tescmd/auth/server.py +108 -0
  21. tescmd/auth/token_store.py +273 -0
  22. tescmd/ble/__init__.py +0 -0
  23. tescmd/cache/__init__.py +6 -0
  24. tescmd/cache/keys.py +51 -0
  25. tescmd/cache/response_cache.py +213 -0
  26. tescmd/cli/__init__.py +0 -0
  27. tescmd/cli/_client.py +603 -0
  28. tescmd/cli/_options.py +126 -0
  29. tescmd/cli/auth.py +682 -0
  30. tescmd/cli/billing.py +240 -0
  31. tescmd/cli/cache.py +85 -0
  32. tescmd/cli/charge.py +610 -0
  33. tescmd/cli/climate.py +501 -0
  34. tescmd/cli/energy.py +385 -0
  35. tescmd/cli/key.py +611 -0
  36. tescmd/cli/main.py +601 -0
  37. tescmd/cli/media.py +146 -0
  38. tescmd/cli/nav.py +242 -0
  39. tescmd/cli/partner.py +112 -0
  40. tescmd/cli/raw.py +75 -0
  41. tescmd/cli/security.py +495 -0
  42. tescmd/cli/setup.py +786 -0
  43. tescmd/cli/sharing.py +188 -0
  44. tescmd/cli/software.py +81 -0
  45. tescmd/cli/status.py +106 -0
  46. tescmd/cli/trunk.py +240 -0
  47. tescmd/cli/user.py +145 -0
  48. tescmd/cli/vehicle.py +837 -0
  49. tescmd/config/__init__.py +0 -0
  50. tescmd/crypto/__init__.py +19 -0
  51. tescmd/crypto/ecdh.py +46 -0
  52. tescmd/crypto/keys.py +122 -0
  53. tescmd/deploy/__init__.py +0 -0
  54. tescmd/deploy/github_pages.py +268 -0
  55. tescmd/models/__init__.py +85 -0
  56. tescmd/models/auth.py +108 -0
  57. tescmd/models/command.py +18 -0
  58. tescmd/models/config.py +63 -0
  59. tescmd/models/energy.py +56 -0
  60. tescmd/models/sharing.py +26 -0
  61. tescmd/models/user.py +37 -0
  62. tescmd/models/vehicle.py +185 -0
  63. tescmd/output/__init__.py +5 -0
  64. tescmd/output/formatter.py +132 -0
  65. tescmd/output/json_output.py +83 -0
  66. tescmd/output/rich_output.py +809 -0
  67. tescmd/protocol/__init__.py +23 -0
  68. tescmd/protocol/commands.py +175 -0
  69. tescmd/protocol/encoder.py +122 -0
  70. tescmd/protocol/metadata.py +116 -0
  71. tescmd/protocol/payloads.py +621 -0
  72. tescmd/protocol/protobuf/__init__.py +6 -0
  73. tescmd/protocol/protobuf/messages.py +564 -0
  74. tescmd/protocol/session.py +318 -0
  75. tescmd/protocol/signer.py +84 -0
  76. tescmd/py.typed +0 -0
  77. tescmd-0.1.2.dist-info/METADATA +458 -0
  78. tescmd-0.1.2.dist-info/RECORD +81 -0
  79. tescmd-0.1.2.dist-info/WHEEL +4 -0
  80. tescmd-0.1.2.dist-info/entry_points.txt +2 -0
  81. 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)