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/vehicle.py
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
"""CLI commands for vehicle operations (list, info, data, location, wake)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextlib
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from tescmd._internal.async_utils import run_async
|
|
12
|
+
from tescmd.api.errors import VehicleAsleepError
|
|
13
|
+
from tescmd.cli._client import (
|
|
14
|
+
TTL_DEFAULT,
|
|
15
|
+
TTL_FAST,
|
|
16
|
+
TTL_SLOW,
|
|
17
|
+
TTL_STATIC,
|
|
18
|
+
cached_api_call,
|
|
19
|
+
cached_vehicle_data,
|
|
20
|
+
execute_command,
|
|
21
|
+
get_vehicle_api,
|
|
22
|
+
require_vin,
|
|
23
|
+
)
|
|
24
|
+
from tescmd.cli._options import global_options
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from tescmd.cli.main import AppContext
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Command group
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
vehicle_group = click.Group("vehicle", help="Vehicle commands")
|
|
35
|
+
|
|
36
|
+
telemetry_group = click.Group("telemetry", help="Fleet telemetry configuration and errors")
|
|
37
|
+
vehicle_group.add_command(telemetry_group)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Commands
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@vehicle_group.command("list")
|
|
46
|
+
@global_options
|
|
47
|
+
def list_cmd(app_ctx: AppContext) -> None:
|
|
48
|
+
"""List all vehicles on the account."""
|
|
49
|
+
run_async(_cmd_list(app_ctx))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def _cmd_list(app_ctx: AppContext) -> None:
|
|
53
|
+
formatter = app_ctx.formatter
|
|
54
|
+
client, api = get_vehicle_api(app_ctx)
|
|
55
|
+
try:
|
|
56
|
+
vehicles = await cached_api_call(
|
|
57
|
+
app_ctx,
|
|
58
|
+
scope="account",
|
|
59
|
+
identifier="global",
|
|
60
|
+
endpoint="vehicle.list",
|
|
61
|
+
fetch=lambda: api.list_vehicles(),
|
|
62
|
+
ttl=TTL_SLOW,
|
|
63
|
+
)
|
|
64
|
+
finally:
|
|
65
|
+
await client.close()
|
|
66
|
+
|
|
67
|
+
if formatter.format == "json":
|
|
68
|
+
formatter.output(vehicles, command="vehicle.list")
|
|
69
|
+
else:
|
|
70
|
+
formatter.rich.vehicle_list(vehicles)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@vehicle_group.command("get")
|
|
74
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
75
|
+
@global_options
|
|
76
|
+
def get_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
77
|
+
"""Fetch basic vehicle info (lightweight, no wake required)."""
|
|
78
|
+
run_async(_cmd_get(app_ctx, vin_positional))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _cmd_get(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
82
|
+
formatter = app_ctx.formatter
|
|
83
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
84
|
+
client, api = get_vehicle_api(app_ctx)
|
|
85
|
+
try:
|
|
86
|
+
vehicle = await cached_api_call(
|
|
87
|
+
app_ctx,
|
|
88
|
+
scope="vin",
|
|
89
|
+
identifier=vin,
|
|
90
|
+
endpoint="vehicle.get",
|
|
91
|
+
fetch=lambda: api.get_vehicle(vin),
|
|
92
|
+
ttl=TTL_DEFAULT,
|
|
93
|
+
)
|
|
94
|
+
finally:
|
|
95
|
+
await client.close()
|
|
96
|
+
|
|
97
|
+
if formatter.format == "json":
|
|
98
|
+
formatter.output(vehicle, command="vehicle.get")
|
|
99
|
+
else:
|
|
100
|
+
state = vehicle.get("state") if isinstance(vehicle, dict) else vehicle.state
|
|
101
|
+
style = "green" if state == "online" else "yellow"
|
|
102
|
+
if isinstance(vehicle, dict):
|
|
103
|
+
name = vehicle.get("display_name") or vehicle.get("vin") or "Unknown"
|
|
104
|
+
v_vin = vehicle.get("vin", "")
|
|
105
|
+
else:
|
|
106
|
+
name = vehicle.display_name or vehicle.vin or "Unknown"
|
|
107
|
+
v_vin = vehicle.vin
|
|
108
|
+
formatter.rich.info(f"{name} [{style}]{state}[/{style}] VIN: {v_vin}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@vehicle_group.command("info")
|
|
112
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
113
|
+
@global_options
|
|
114
|
+
def info_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
115
|
+
"""Show all vehicle data."""
|
|
116
|
+
run_async(_cmd_info(app_ctx, vin_positional))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def _cmd_info(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
120
|
+
formatter = app_ctx.formatter
|
|
121
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
122
|
+
client, api = get_vehicle_api(app_ctx)
|
|
123
|
+
try:
|
|
124
|
+
vdata = await cached_vehicle_data(app_ctx, api, vin)
|
|
125
|
+
finally:
|
|
126
|
+
await client.close()
|
|
127
|
+
|
|
128
|
+
if formatter.format == "json":
|
|
129
|
+
formatter.output(vdata, command="vehicle.info")
|
|
130
|
+
else:
|
|
131
|
+
formatter.rich.vehicle_data(vdata)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@vehicle_group.command("data")
|
|
135
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
136
|
+
@click.option("--endpoints", default=None, help="Comma-separated endpoint filter")
|
|
137
|
+
@global_options
|
|
138
|
+
def data_cmd(app_ctx: AppContext, vin_positional: str | None, endpoints: str | None) -> None:
|
|
139
|
+
"""Fetch vehicle data filtered by endpoint."""
|
|
140
|
+
run_async(_cmd_data(app_ctx, vin_positional, endpoints))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def _cmd_data(
|
|
144
|
+
app_ctx: AppContext, vin_positional: str | None, endpoints: str | None
|
|
145
|
+
) -> None:
|
|
146
|
+
formatter = app_ctx.formatter
|
|
147
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
148
|
+
endpoint_list: list[str] | None = None
|
|
149
|
+
if endpoints:
|
|
150
|
+
endpoint_list = [e.strip() for e in endpoints.split(",")]
|
|
151
|
+
|
|
152
|
+
client, api = get_vehicle_api(app_ctx)
|
|
153
|
+
try:
|
|
154
|
+
vdata = await cached_vehicle_data(app_ctx, api, vin, endpoints=endpoint_list)
|
|
155
|
+
finally:
|
|
156
|
+
await client.close()
|
|
157
|
+
|
|
158
|
+
if formatter.format == "json":
|
|
159
|
+
formatter.output(vdata, command="vehicle.data")
|
|
160
|
+
else:
|
|
161
|
+
formatter.rich.vehicle_data(vdata)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@vehicle_group.command("location")
|
|
165
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
166
|
+
@global_options
|
|
167
|
+
def location_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
168
|
+
"""Show the vehicle's current location."""
|
|
169
|
+
run_async(_cmd_location(app_ctx, vin_positional))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def _cmd_location(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
173
|
+
formatter = app_ctx.formatter
|
|
174
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
175
|
+
client, api = get_vehicle_api(app_ctx)
|
|
176
|
+
try:
|
|
177
|
+
vdata = await cached_vehicle_data(app_ctx, api, vin, endpoints=["drive_state"])
|
|
178
|
+
finally:
|
|
179
|
+
await client.close()
|
|
180
|
+
|
|
181
|
+
if formatter.format == "json":
|
|
182
|
+
ds = vdata.drive_state
|
|
183
|
+
formatter.output(
|
|
184
|
+
ds.model_dump(exclude_none=True) if ds else {},
|
|
185
|
+
command="vehicle.location",
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
if vdata.drive_state:
|
|
189
|
+
formatter.rich.location(vdata.drive_state)
|
|
190
|
+
else:
|
|
191
|
+
formatter.rich.info("No drive state data available.")
|
|
192
|
+
if vdata.state == "online":
|
|
193
|
+
formatter.rich.info(
|
|
194
|
+
"[dim]Location requires a vehicle command key."
|
|
195
|
+
" Run [cyan]tescmd setup[/cyan] and choose"
|
|
196
|
+
" full control to enable location access.[/dim]"
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
formatter.rich.info(
|
|
200
|
+
"[dim]The vehicle may be asleep."
|
|
201
|
+
" Try [cyan]tescmd vehicle wake[/cyan] first.[/dim]"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@vehicle_group.command("wake")
|
|
206
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
207
|
+
@click.option("--wait", is_flag=True, help="Wait for vehicle to come online")
|
|
208
|
+
@click.option("--timeout", type=int, default=30, help="Timeout in seconds when using --wait")
|
|
209
|
+
@global_options
|
|
210
|
+
def wake_cmd(
|
|
211
|
+
app_ctx: AppContext,
|
|
212
|
+
vin_positional: str | None,
|
|
213
|
+
wait: bool,
|
|
214
|
+
timeout: int,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Wake up the vehicle."""
|
|
217
|
+
run_async(_cmd_wake(app_ctx, vin_positional, wait, timeout))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
async def _cmd_wake(
|
|
221
|
+
app_ctx: AppContext,
|
|
222
|
+
vin_positional: str | None,
|
|
223
|
+
wait: bool,
|
|
224
|
+
timeout: int,
|
|
225
|
+
) -> None:
|
|
226
|
+
formatter = app_ctx.formatter
|
|
227
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
228
|
+
client, api = get_vehicle_api(app_ctx)
|
|
229
|
+
try:
|
|
230
|
+
vehicle = await api.wake(vin)
|
|
231
|
+
|
|
232
|
+
if wait and vehicle.state != "online":
|
|
233
|
+
elapsed = 0
|
|
234
|
+
while elapsed < timeout and vehicle.state != "online":
|
|
235
|
+
await asyncio.sleep(2)
|
|
236
|
+
elapsed += 2
|
|
237
|
+
with contextlib.suppress(VehicleAsleepError):
|
|
238
|
+
vehicle = await api.wake(vin)
|
|
239
|
+
finally:
|
|
240
|
+
await client.close()
|
|
241
|
+
|
|
242
|
+
if formatter.format == "json":
|
|
243
|
+
formatter.output(vehicle, command="vehicle.wake")
|
|
244
|
+
else:
|
|
245
|
+
state = vehicle.state
|
|
246
|
+
style = "green" if state == "online" else "yellow"
|
|
247
|
+
formatter.rich.info(f"Vehicle state: [{style}]{state}[/{style}]")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
# Vehicle extras
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@vehicle_group.command("rename")
|
|
256
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
257
|
+
@click.argument("name")
|
|
258
|
+
@global_options
|
|
259
|
+
def rename_cmd(app_ctx: AppContext, vin_positional: str | None, name: str) -> None:
|
|
260
|
+
"""Rename the vehicle."""
|
|
261
|
+
run_async(
|
|
262
|
+
execute_command(
|
|
263
|
+
app_ctx,
|
|
264
|
+
vin_positional,
|
|
265
|
+
"set_vehicle_name",
|
|
266
|
+
"vehicle.rename",
|
|
267
|
+
body={"vehicle_name": name},
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@vehicle_group.command("mobile-access")
|
|
273
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
274
|
+
@global_options
|
|
275
|
+
def mobile_access_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
276
|
+
"""Check if mobile access is enabled."""
|
|
277
|
+
run_async(_cmd_mobile_access(app_ctx, vin_positional))
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
async def _cmd_mobile_access(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
281
|
+
formatter = app_ctx.formatter
|
|
282
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
283
|
+
client, api = get_vehicle_api(app_ctx)
|
|
284
|
+
try:
|
|
285
|
+
result = await cached_api_call(
|
|
286
|
+
app_ctx,
|
|
287
|
+
scope="vin",
|
|
288
|
+
identifier=vin,
|
|
289
|
+
endpoint="vehicle.mobile-access",
|
|
290
|
+
fetch=lambda: api.mobile_enabled(vin),
|
|
291
|
+
ttl=TTL_DEFAULT,
|
|
292
|
+
)
|
|
293
|
+
finally:
|
|
294
|
+
await client.close()
|
|
295
|
+
|
|
296
|
+
# Result is bool on miss, {"_value": bool} on hit
|
|
297
|
+
enabled = result.get("_value") if isinstance(result, dict) else result
|
|
298
|
+
if formatter.format == "json":
|
|
299
|
+
formatter.output({"mobile_enabled": enabled}, command="vehicle.mobile-access")
|
|
300
|
+
else:
|
|
301
|
+
label = "[green]enabled[/green]" if enabled else "[red]disabled[/red]"
|
|
302
|
+
formatter.rich.info(f"Mobile access: {label}")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@vehicle_group.command("nearby-chargers")
|
|
306
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
307
|
+
@global_options
|
|
308
|
+
def nearby_chargers_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
309
|
+
"""Show nearby Superchargers and destination chargers."""
|
|
310
|
+
run_async(_cmd_nearby_chargers(app_ctx, vin_positional))
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
async def _cmd_nearby_chargers(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
314
|
+
formatter = app_ctx.formatter
|
|
315
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
316
|
+
client, api = get_vehicle_api(app_ctx)
|
|
317
|
+
try:
|
|
318
|
+
data = await cached_api_call(
|
|
319
|
+
app_ctx,
|
|
320
|
+
scope="vin",
|
|
321
|
+
identifier=vin,
|
|
322
|
+
endpoint="vehicle.nearby-chargers",
|
|
323
|
+
fetch=lambda: api.nearby_charging_sites(vin),
|
|
324
|
+
ttl=TTL_FAST,
|
|
325
|
+
)
|
|
326
|
+
finally:
|
|
327
|
+
await client.close()
|
|
328
|
+
|
|
329
|
+
if formatter.format == "json":
|
|
330
|
+
formatter.output(data, command="vehicle.nearby-chargers")
|
|
331
|
+
else:
|
|
332
|
+
formatter.rich.nearby_chargers(data)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@vehicle_group.command("alerts")
|
|
336
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
337
|
+
@global_options
|
|
338
|
+
def alerts_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
339
|
+
"""Show recent vehicle alerts."""
|
|
340
|
+
run_async(_cmd_alerts(app_ctx, vin_positional))
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
async def _cmd_alerts(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
344
|
+
formatter = app_ctx.formatter
|
|
345
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
346
|
+
client, api = get_vehicle_api(app_ctx)
|
|
347
|
+
try:
|
|
348
|
+
alerts = await cached_api_call(
|
|
349
|
+
app_ctx,
|
|
350
|
+
scope="vin",
|
|
351
|
+
identifier=vin,
|
|
352
|
+
endpoint="vehicle.alerts",
|
|
353
|
+
fetch=lambda: api.recent_alerts(vin),
|
|
354
|
+
ttl=TTL_DEFAULT,
|
|
355
|
+
)
|
|
356
|
+
finally:
|
|
357
|
+
await client.close()
|
|
358
|
+
|
|
359
|
+
if formatter.format == "json":
|
|
360
|
+
formatter.output(alerts, command="vehicle.alerts")
|
|
361
|
+
else:
|
|
362
|
+
if alerts:
|
|
363
|
+
for alert in alerts:
|
|
364
|
+
name = alert.get("name", "Unknown")
|
|
365
|
+
ts = alert.get("time", "")
|
|
366
|
+
formatter.rich.info(f" {name} [dim]{ts}[/dim]")
|
|
367
|
+
else:
|
|
368
|
+
formatter.rich.info("[dim]No recent alerts.[/dim]")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@vehicle_group.command("release-notes")
|
|
372
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
373
|
+
@global_options
|
|
374
|
+
def release_notes_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
375
|
+
"""Show firmware release notes."""
|
|
376
|
+
run_async(_cmd_release_notes(app_ctx, vin_positional))
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
async def _cmd_release_notes(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
380
|
+
formatter = app_ctx.formatter
|
|
381
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
382
|
+
client, api = get_vehicle_api(app_ctx)
|
|
383
|
+
try:
|
|
384
|
+
data = await cached_api_call(
|
|
385
|
+
app_ctx,
|
|
386
|
+
scope="vin",
|
|
387
|
+
identifier=vin,
|
|
388
|
+
endpoint="vehicle.release-notes",
|
|
389
|
+
fetch=lambda: api.release_notes(vin),
|
|
390
|
+
ttl=TTL_SLOW,
|
|
391
|
+
)
|
|
392
|
+
finally:
|
|
393
|
+
await client.close()
|
|
394
|
+
|
|
395
|
+
if formatter.format == "json":
|
|
396
|
+
formatter.output(data, command="vehicle.release-notes")
|
|
397
|
+
else:
|
|
398
|
+
formatter.rich.vehicle_release_notes(data)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@vehicle_group.command("service")
|
|
402
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
403
|
+
@global_options
|
|
404
|
+
def service_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
405
|
+
"""Show vehicle service data."""
|
|
406
|
+
run_async(_cmd_service(app_ctx, vin_positional))
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
async def _cmd_service(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
410
|
+
formatter = app_ctx.formatter
|
|
411
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
412
|
+
client, api = get_vehicle_api(app_ctx)
|
|
413
|
+
try:
|
|
414
|
+
data = await cached_api_call(
|
|
415
|
+
app_ctx,
|
|
416
|
+
scope="vin",
|
|
417
|
+
identifier=vin,
|
|
418
|
+
endpoint="vehicle.service",
|
|
419
|
+
fetch=lambda: api.service_data(vin),
|
|
420
|
+
ttl=TTL_SLOW,
|
|
421
|
+
)
|
|
422
|
+
finally:
|
|
423
|
+
await client.close()
|
|
424
|
+
|
|
425
|
+
if formatter.format == "json":
|
|
426
|
+
formatter.output(data, command="vehicle.service")
|
|
427
|
+
else:
|
|
428
|
+
formatter.rich.vehicle_service(data)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@vehicle_group.command("drivers")
|
|
432
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
433
|
+
@global_options
|
|
434
|
+
def drivers_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
435
|
+
"""List drivers associated with the vehicle."""
|
|
436
|
+
run_async(_cmd_drivers(app_ctx, vin_positional))
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
async def _cmd_drivers(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
440
|
+
formatter = app_ctx.formatter
|
|
441
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
442
|
+
client, api = get_vehicle_api(app_ctx)
|
|
443
|
+
try:
|
|
444
|
+
drivers = await cached_api_call(
|
|
445
|
+
app_ctx,
|
|
446
|
+
scope="vin",
|
|
447
|
+
identifier=vin,
|
|
448
|
+
endpoint="vehicle.drivers",
|
|
449
|
+
fetch=lambda: api.list_drivers(vin),
|
|
450
|
+
ttl=TTL_SLOW,
|
|
451
|
+
)
|
|
452
|
+
finally:
|
|
453
|
+
await client.close()
|
|
454
|
+
|
|
455
|
+
if formatter.format == "json":
|
|
456
|
+
formatter.output(drivers, command="vehicle.drivers")
|
|
457
|
+
else:
|
|
458
|
+
if drivers:
|
|
459
|
+
for d in drivers:
|
|
460
|
+
email = (d.get("email") if isinstance(d, dict) else d.email) or "unknown"
|
|
461
|
+
status = (d.get("status") if isinstance(d, dict) else d.status) or ""
|
|
462
|
+
formatter.rich.info(f" {email} [dim]{status}[/dim]")
|
|
463
|
+
else:
|
|
464
|
+
formatter.rich.info("[dim]No drivers found.[/dim]")
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@vehicle_group.command("calendar")
|
|
468
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
469
|
+
@click.argument("calendar_data")
|
|
470
|
+
@global_options
|
|
471
|
+
def calendar_cmd(app_ctx: AppContext, vin_positional: str | None, calendar_data: str) -> None:
|
|
472
|
+
"""Send calendar entries to the vehicle.
|
|
473
|
+
|
|
474
|
+
CALENDAR_DATA should be a JSON string of calendar entries.
|
|
475
|
+
"""
|
|
476
|
+
run_async(
|
|
477
|
+
execute_command(
|
|
478
|
+
app_ctx,
|
|
479
|
+
vin_positional,
|
|
480
|
+
"upcoming_calendar_entries",
|
|
481
|
+
"vehicle.calendar",
|
|
482
|
+
body={"calendar_data": calendar_data},
|
|
483
|
+
)
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
# ---------------------------------------------------------------------------
|
|
488
|
+
# Power management commands
|
|
489
|
+
# ---------------------------------------------------------------------------
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# ---------------------------------------------------------------------------
|
|
493
|
+
# Extended vehicle data endpoints
|
|
494
|
+
# ---------------------------------------------------------------------------
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@vehicle_group.command("subscriptions")
|
|
498
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
499
|
+
@global_options
|
|
500
|
+
def subscriptions_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
501
|
+
"""Check subscription eligibility for the vehicle."""
|
|
502
|
+
run_async(_cmd_subscriptions(app_ctx, vin_positional))
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
async def _cmd_subscriptions(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
506
|
+
formatter = app_ctx.formatter
|
|
507
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
508
|
+
client, api = get_vehicle_api(app_ctx)
|
|
509
|
+
try:
|
|
510
|
+
data = await cached_api_call(
|
|
511
|
+
app_ctx,
|
|
512
|
+
scope="vin",
|
|
513
|
+
identifier=vin,
|
|
514
|
+
endpoint="vehicle.subscriptions",
|
|
515
|
+
fetch=lambda: api.eligible_subscriptions(vin),
|
|
516
|
+
ttl=TTL_SLOW,
|
|
517
|
+
)
|
|
518
|
+
finally:
|
|
519
|
+
await client.close()
|
|
520
|
+
|
|
521
|
+
if formatter.format == "json":
|
|
522
|
+
formatter.output(data, command="vehicle.subscriptions")
|
|
523
|
+
else:
|
|
524
|
+
formatter.rich.vehicle_subscriptions(data)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
@vehicle_group.command("upgrades")
|
|
528
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
529
|
+
@global_options
|
|
530
|
+
def upgrades_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
531
|
+
"""Check upgrade eligibility for the vehicle."""
|
|
532
|
+
run_async(_cmd_upgrades(app_ctx, vin_positional))
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
async def _cmd_upgrades(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
536
|
+
formatter = app_ctx.formatter
|
|
537
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
538
|
+
client, api = get_vehicle_api(app_ctx)
|
|
539
|
+
try:
|
|
540
|
+
data = await cached_api_call(
|
|
541
|
+
app_ctx,
|
|
542
|
+
scope="vin",
|
|
543
|
+
identifier=vin,
|
|
544
|
+
endpoint="vehicle.upgrades",
|
|
545
|
+
fetch=lambda: api.eligible_upgrades(vin),
|
|
546
|
+
ttl=TTL_SLOW,
|
|
547
|
+
)
|
|
548
|
+
finally:
|
|
549
|
+
await client.close()
|
|
550
|
+
|
|
551
|
+
if formatter.format == "json":
|
|
552
|
+
formatter.output(data, command="vehicle.upgrades")
|
|
553
|
+
else:
|
|
554
|
+
formatter.rich.vehicle_upgrades(data)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@vehicle_group.command("options")
|
|
558
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
559
|
+
@global_options
|
|
560
|
+
def options_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
561
|
+
"""Fetch vehicle option codes."""
|
|
562
|
+
run_async(_cmd_options(app_ctx, vin_positional))
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
async def _cmd_options(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
566
|
+
formatter = app_ctx.formatter
|
|
567
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
568
|
+
client, api = get_vehicle_api(app_ctx)
|
|
569
|
+
try:
|
|
570
|
+
data = await cached_api_call(
|
|
571
|
+
app_ctx,
|
|
572
|
+
scope="vin",
|
|
573
|
+
identifier=vin,
|
|
574
|
+
endpoint="vehicle.options",
|
|
575
|
+
fetch=lambda: api.options(vin),
|
|
576
|
+
ttl=TTL_STATIC,
|
|
577
|
+
)
|
|
578
|
+
finally:
|
|
579
|
+
await client.close()
|
|
580
|
+
|
|
581
|
+
if formatter.format == "json":
|
|
582
|
+
formatter.output(data, command="vehicle.options")
|
|
583
|
+
else:
|
|
584
|
+
formatter.rich.vehicle_options(data)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
@vehicle_group.command("specs")
|
|
588
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
589
|
+
@global_options
|
|
590
|
+
def specs_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
591
|
+
"""Fetch vehicle specifications (partner tokens, $0.10/call)."""
|
|
592
|
+
run_async(_cmd_specs(app_ctx, vin_positional))
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
async def _cmd_specs(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
596
|
+
formatter = app_ctx.formatter
|
|
597
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
598
|
+
client, api = get_vehicle_api(app_ctx)
|
|
599
|
+
try:
|
|
600
|
+
data = await cached_api_call(
|
|
601
|
+
app_ctx,
|
|
602
|
+
scope="vin",
|
|
603
|
+
identifier=vin,
|
|
604
|
+
endpoint="vehicle.specs",
|
|
605
|
+
fetch=lambda: api.specs(vin),
|
|
606
|
+
ttl=TTL_STATIC,
|
|
607
|
+
)
|
|
608
|
+
finally:
|
|
609
|
+
await client.close()
|
|
610
|
+
|
|
611
|
+
if formatter.format == "json":
|
|
612
|
+
formatter.output(data, command="vehicle.specs")
|
|
613
|
+
else:
|
|
614
|
+
formatter.rich.vehicle_specs(data)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
@vehicle_group.command("warranty")
|
|
618
|
+
@global_options
|
|
619
|
+
def warranty_cmd(app_ctx: AppContext) -> None:
|
|
620
|
+
"""Fetch warranty details for the account."""
|
|
621
|
+
run_async(_cmd_warranty(app_ctx))
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
async def _cmd_warranty(app_ctx: AppContext) -> None:
|
|
625
|
+
formatter = app_ctx.formatter
|
|
626
|
+
client, api = get_vehicle_api(app_ctx)
|
|
627
|
+
try:
|
|
628
|
+
data = await cached_api_call(
|
|
629
|
+
app_ctx,
|
|
630
|
+
scope="account",
|
|
631
|
+
identifier="global",
|
|
632
|
+
endpoint="vehicle.warranty",
|
|
633
|
+
fetch=lambda: api.warranty_details(),
|
|
634
|
+
ttl=TTL_STATIC,
|
|
635
|
+
)
|
|
636
|
+
finally:
|
|
637
|
+
await client.close()
|
|
638
|
+
|
|
639
|
+
if formatter.format == "json":
|
|
640
|
+
formatter.output(data, command="vehicle.warranty")
|
|
641
|
+
else:
|
|
642
|
+
formatter.rich.vehicle_warranty(data)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
@vehicle_group.command("fleet-status")
|
|
646
|
+
@global_options
|
|
647
|
+
def fleet_status_cmd(app_ctx: AppContext) -> None:
|
|
648
|
+
"""Fetch fleet status for all vehicles."""
|
|
649
|
+
run_async(_cmd_fleet_status(app_ctx))
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
async def _cmd_fleet_status(app_ctx: AppContext) -> None:
|
|
653
|
+
formatter = app_ctx.formatter
|
|
654
|
+
client, api = get_vehicle_api(app_ctx)
|
|
655
|
+
try:
|
|
656
|
+
data = await cached_api_call(
|
|
657
|
+
app_ctx,
|
|
658
|
+
scope="account",
|
|
659
|
+
identifier="global",
|
|
660
|
+
endpoint="vehicle.fleet-status",
|
|
661
|
+
fetch=lambda: api.fleet_status(),
|
|
662
|
+
ttl=TTL_SLOW,
|
|
663
|
+
)
|
|
664
|
+
finally:
|
|
665
|
+
await client.close()
|
|
666
|
+
|
|
667
|
+
if formatter.format == "json":
|
|
668
|
+
formatter.output(data, command="vehicle.fleet-status")
|
|
669
|
+
else:
|
|
670
|
+
formatter.rich.fleet_status(data)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
@telemetry_group.command("config")
|
|
674
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
675
|
+
@global_options
|
|
676
|
+
def telemetry_config_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
677
|
+
"""Fetch fleet telemetry configuration for a vehicle."""
|
|
678
|
+
run_async(_cmd_telemetry_config(app_ctx, vin_positional))
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
async def _cmd_telemetry_config(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
682
|
+
formatter = app_ctx.formatter
|
|
683
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
684
|
+
client, api = get_vehicle_api(app_ctx)
|
|
685
|
+
try:
|
|
686
|
+
data = await cached_api_call(
|
|
687
|
+
app_ctx,
|
|
688
|
+
scope="vin",
|
|
689
|
+
identifier=vin,
|
|
690
|
+
endpoint="vehicle.telemetry.config",
|
|
691
|
+
fetch=lambda: api.fleet_telemetry_config(vin),
|
|
692
|
+
ttl=TTL_SLOW,
|
|
693
|
+
)
|
|
694
|
+
finally:
|
|
695
|
+
await client.close()
|
|
696
|
+
|
|
697
|
+
if formatter.format == "json":
|
|
698
|
+
formatter.output(data, command="vehicle.telemetry.config")
|
|
699
|
+
else:
|
|
700
|
+
formatter.rich.telemetry_config(data)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
@telemetry_group.command("create")
|
|
704
|
+
@click.argument("config_json")
|
|
705
|
+
@global_options
|
|
706
|
+
def telemetry_config_create_cmd(app_ctx: AppContext, config_json: str) -> None:
|
|
707
|
+
"""Create or update fleet telemetry server configuration (CONFIG_JSON is a JSON string)."""
|
|
708
|
+
import json
|
|
709
|
+
|
|
710
|
+
from tescmd.api.errors import ConfigError
|
|
711
|
+
|
|
712
|
+
try:
|
|
713
|
+
config = json.loads(config_json)
|
|
714
|
+
except json.JSONDecodeError as e:
|
|
715
|
+
raise ConfigError(f"Invalid JSON in CONFIG_JSON: {e}") from e
|
|
716
|
+
run_async(_cmd_telemetry_config_create(app_ctx, config))
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
async def _cmd_telemetry_config_create(app_ctx: AppContext, config: dict[str, object]) -> None:
|
|
720
|
+
formatter = app_ctx.formatter
|
|
721
|
+
client, api = get_vehicle_api(app_ctx)
|
|
722
|
+
try:
|
|
723
|
+
data = await api.fleet_telemetry_config_create(config=config)
|
|
724
|
+
finally:
|
|
725
|
+
await client.close()
|
|
726
|
+
|
|
727
|
+
if formatter.format == "json":
|
|
728
|
+
formatter.output(data, command="vehicle.telemetry.create")
|
|
729
|
+
else:
|
|
730
|
+
formatter.rich.info("Fleet telemetry config created/updated.")
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
@telemetry_group.command("delete")
|
|
734
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
735
|
+
@click.option(
|
|
736
|
+
"--confirm",
|
|
737
|
+
is_flag=True,
|
|
738
|
+
required=True,
|
|
739
|
+
help="Required flag to confirm config deletion",
|
|
740
|
+
)
|
|
741
|
+
@global_options
|
|
742
|
+
def telemetry_config_delete_cmd(
|
|
743
|
+
app_ctx: AppContext, vin_positional: str | None, confirm: bool
|
|
744
|
+
) -> None:
|
|
745
|
+
"""Remove fleet telemetry configuration from a vehicle (DESTRUCTIVE).
|
|
746
|
+
|
|
747
|
+
Requires --confirm flag.
|
|
748
|
+
"""
|
|
749
|
+
run_async(_cmd_telemetry_config_delete(app_ctx, vin_positional))
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
async def _cmd_telemetry_config_delete(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
753
|
+
formatter = app_ctx.formatter
|
|
754
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
755
|
+
client, api = get_vehicle_api(app_ctx)
|
|
756
|
+
try:
|
|
757
|
+
data = await api.fleet_telemetry_config_delete(vin)
|
|
758
|
+
finally:
|
|
759
|
+
await client.close()
|
|
760
|
+
|
|
761
|
+
if formatter.format == "json":
|
|
762
|
+
formatter.output(data, command="vehicle.telemetry.delete")
|
|
763
|
+
else:
|
|
764
|
+
formatter.rich.info("Fleet telemetry config deleted.")
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
@telemetry_group.command("errors")
|
|
768
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
769
|
+
@global_options
|
|
770
|
+
def telemetry_errors_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
771
|
+
"""Fetch fleet telemetry errors for a vehicle."""
|
|
772
|
+
run_async(_cmd_telemetry_errors(app_ctx, vin_positional))
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
async def _cmd_telemetry_errors(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
776
|
+
formatter = app_ctx.formatter
|
|
777
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
778
|
+
client, api = get_vehicle_api(app_ctx)
|
|
779
|
+
try:
|
|
780
|
+
data = await cached_api_call(
|
|
781
|
+
app_ctx,
|
|
782
|
+
scope="vin",
|
|
783
|
+
identifier=vin,
|
|
784
|
+
endpoint="vehicle.telemetry.errors",
|
|
785
|
+
fetch=lambda: api.fleet_telemetry_errors(vin),
|
|
786
|
+
ttl=TTL_SLOW,
|
|
787
|
+
)
|
|
788
|
+
finally:
|
|
789
|
+
await client.close()
|
|
790
|
+
|
|
791
|
+
if formatter.format == "json":
|
|
792
|
+
formatter.output(data, command="vehicle.telemetry.errors")
|
|
793
|
+
else:
|
|
794
|
+
formatter.rich.telemetry_errors(data)
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
# ---------------------------------------------------------------------------
|
|
798
|
+
# Power management commands
|
|
799
|
+
# ---------------------------------------------------------------------------
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
@vehicle_group.command("low-power")
|
|
803
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
804
|
+
@click.option("--on/--off", default=True, help="Enable or disable low power mode")
|
|
805
|
+
@global_options
|
|
806
|
+
def low_power_cmd(app_ctx: AppContext, vin_positional: str | None, on: bool) -> None:
|
|
807
|
+
"""Enable or disable low power mode."""
|
|
808
|
+
state = "enabled" if on else "disabled"
|
|
809
|
+
run_async(
|
|
810
|
+
execute_command(
|
|
811
|
+
app_ctx,
|
|
812
|
+
vin_positional,
|
|
813
|
+
"set_low_power_mode",
|
|
814
|
+
"vehicle.low-power",
|
|
815
|
+
body={"enable": on},
|
|
816
|
+
success_message=f"Low power mode {state}.",
|
|
817
|
+
)
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
@vehicle_group.command("accessory-power")
|
|
822
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
823
|
+
@click.option("--on/--off", default=True, help="Keep USB/outlets powered after exit")
|
|
824
|
+
@global_options
|
|
825
|
+
def accessory_power_cmd(app_ctx: AppContext, vin_positional: str | None, on: bool) -> None:
|
|
826
|
+
"""Keep accessory power (USB/outlets) active after exiting the vehicle."""
|
|
827
|
+
state = "enabled" if on else "disabled"
|
|
828
|
+
run_async(
|
|
829
|
+
execute_command(
|
|
830
|
+
app_ctx,
|
|
831
|
+
vin_positional,
|
|
832
|
+
"keep_accessory_power_mode",
|
|
833
|
+
"vehicle.accessory-power",
|
|
834
|
+
body={"enable": on},
|
|
835
|
+
success_message=f"Accessory power mode {state}.",
|
|
836
|
+
)
|
|
837
|
+
)
|