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/climate.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
"""CLI commands for climate 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.cli._client import (
|
|
11
|
+
auto_wake,
|
|
12
|
+
cached_vehicle_data,
|
|
13
|
+
execute_command,
|
|
14
|
+
get_command_api,
|
|
15
|
+
get_vehicle_api,
|
|
16
|
+
invalidate_cache_for_vin,
|
|
17
|
+
require_vin,
|
|
18
|
+
)
|
|
19
|
+
from tescmd.cli._options import global_options
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from tescmd.cli.main import AppContext
|
|
23
|
+
|
|
24
|
+
climate_group = click.Group("climate", help="Climate and comfort commands")
|
|
25
|
+
|
|
26
|
+
# Tesla's internal seat position indices
|
|
27
|
+
SEAT_MAP: dict[str, int] = {
|
|
28
|
+
"driver": 0,
|
|
29
|
+
"passenger": 1,
|
|
30
|
+
"rear-left": 2,
|
|
31
|
+
"rear-center": 4,
|
|
32
|
+
"rear-right": 5,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Climate keeper mode string → integer
|
|
36
|
+
KEEPER_MODE_MAP: dict[str, int] = {
|
|
37
|
+
"off": 0,
|
|
38
|
+
"on": 1,
|
|
39
|
+
"dog": 2,
|
|
40
|
+
"camp": 3,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _f_to_c(fahrenheit: float) -> float:
|
|
45
|
+
"""Convert Fahrenheit to Celsius."""
|
|
46
|
+
return (fahrenheit - 32.0) * 5.0 / 9.0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Status (read via VehicleAPI)
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@climate_group.command("status")
|
|
55
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
56
|
+
@global_options
|
|
57
|
+
def status_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
58
|
+
"""Show current climate status."""
|
|
59
|
+
run_async(_cmd_status(app_ctx, vin_positional))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def _cmd_status(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
63
|
+
formatter = app_ctx.formatter
|
|
64
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
65
|
+
client, api = get_vehicle_api(app_ctx)
|
|
66
|
+
try:
|
|
67
|
+
vdata = await cached_vehicle_data(app_ctx, api, vin, endpoints=["climate_state"])
|
|
68
|
+
finally:
|
|
69
|
+
await client.close()
|
|
70
|
+
|
|
71
|
+
if formatter.format == "json":
|
|
72
|
+
cs = vdata.climate_state
|
|
73
|
+
formatter.output(
|
|
74
|
+
cs.model_dump(exclude_none=True) if cs else {},
|
|
75
|
+
command="climate.status",
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
if vdata.climate_state:
|
|
79
|
+
formatter.rich.climate_status(vdata.climate_state)
|
|
80
|
+
else:
|
|
81
|
+
formatter.rich.info("No climate state data available.")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Simple on/off commands
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@climate_group.command("on")
|
|
90
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
91
|
+
@global_options
|
|
92
|
+
def on_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
93
|
+
"""Turn on climate control (auto conditioning)."""
|
|
94
|
+
run_async(
|
|
95
|
+
execute_command(
|
|
96
|
+
app_ctx,
|
|
97
|
+
vin_positional,
|
|
98
|
+
"auto_conditioning_start",
|
|
99
|
+
"climate.on",
|
|
100
|
+
success_message="Climate control turned on.",
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@climate_group.command("off")
|
|
106
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
107
|
+
@global_options
|
|
108
|
+
def off_cmd(app_ctx: AppContext, vin_positional: str | None) -> None:
|
|
109
|
+
"""Turn off climate control (auto conditioning)."""
|
|
110
|
+
run_async(
|
|
111
|
+
execute_command(
|
|
112
|
+
app_ctx,
|
|
113
|
+
vin_positional,
|
|
114
|
+
"auto_conditioning_stop",
|
|
115
|
+
"climate.off",
|
|
116
|
+
success_message="Climate control turned off.",
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# Parameterised commands
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@climate_group.command("set")
|
|
127
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
128
|
+
@click.argument("temp", type=float)
|
|
129
|
+
@click.option("--passenger", type=float, default=None, help="Separate passenger temperature")
|
|
130
|
+
@click.option("--celsius", is_flag=True, default=False, help="Input temperature is in Celsius")
|
|
131
|
+
@global_options
|
|
132
|
+
def set_cmd(
|
|
133
|
+
app_ctx: AppContext,
|
|
134
|
+
vin_positional: str | None,
|
|
135
|
+
temp: float,
|
|
136
|
+
passenger: float | None,
|
|
137
|
+
celsius: bool,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Set cabin temperature to TEMP (default: Fahrenheit)."""
|
|
140
|
+
run_async(_cmd_set(app_ctx, vin_positional, temp, passenger, celsius))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def _cmd_set(
|
|
144
|
+
app_ctx: AppContext,
|
|
145
|
+
vin_positional: str | None,
|
|
146
|
+
temp: float,
|
|
147
|
+
passenger: float | None,
|
|
148
|
+
celsius: bool,
|
|
149
|
+
) -> None:
|
|
150
|
+
formatter = app_ctx.formatter
|
|
151
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
152
|
+
|
|
153
|
+
driver_c = temp if celsius else _f_to_c(temp)
|
|
154
|
+
passenger_c = driver_c
|
|
155
|
+
if passenger is not None:
|
|
156
|
+
passenger_c = passenger if celsius else _f_to_c(passenger)
|
|
157
|
+
|
|
158
|
+
client, vehicle_api, cmd_api = get_command_api(app_ctx)
|
|
159
|
+
try:
|
|
160
|
+
result = await auto_wake(
|
|
161
|
+
formatter,
|
|
162
|
+
vehicle_api,
|
|
163
|
+
vin,
|
|
164
|
+
lambda: cmd_api.set_temps(vin, driver_temp=driver_c, passenger_temp=passenger_c),
|
|
165
|
+
auto=app_ctx.auto_wake,
|
|
166
|
+
)
|
|
167
|
+
finally:
|
|
168
|
+
await client.close()
|
|
169
|
+
|
|
170
|
+
invalidate_cache_for_vin(app_ctx, vin)
|
|
171
|
+
|
|
172
|
+
if formatter.format == "json":
|
|
173
|
+
formatter.output(result, command="climate.set")
|
|
174
|
+
else:
|
|
175
|
+
msg = result.response.reason or "Temperature set."
|
|
176
|
+
formatter.rich.command_result(result.response.result, msg)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@climate_group.command("precondition")
|
|
180
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
181
|
+
@click.option("--on/--off", default=True, help="Enable or disable max preconditioning")
|
|
182
|
+
@global_options
|
|
183
|
+
def precondition_cmd(app_ctx: AppContext, vin_positional: str | None, on: bool) -> None:
|
|
184
|
+
"""Enable or disable max preconditioning."""
|
|
185
|
+
run_async(_cmd_precondition(app_ctx, vin_positional, on))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
async def _cmd_precondition(app_ctx: AppContext, vin_positional: str | None, on: bool) -> None:
|
|
189
|
+
formatter = app_ctx.formatter
|
|
190
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
191
|
+
client, vehicle_api, cmd_api = get_command_api(app_ctx)
|
|
192
|
+
try:
|
|
193
|
+
result = await auto_wake(
|
|
194
|
+
formatter,
|
|
195
|
+
vehicle_api,
|
|
196
|
+
vin,
|
|
197
|
+
lambda: cmd_api.set_preconditioning_max(vin, on=on),
|
|
198
|
+
auto=app_ctx.auto_wake,
|
|
199
|
+
)
|
|
200
|
+
finally:
|
|
201
|
+
await client.close()
|
|
202
|
+
|
|
203
|
+
invalidate_cache_for_vin(app_ctx, vin)
|
|
204
|
+
|
|
205
|
+
if formatter.format == "json":
|
|
206
|
+
formatter.output(result, command="climate.precondition")
|
|
207
|
+
else:
|
|
208
|
+
state = "enabled" if on else "disabled"
|
|
209
|
+
msg = result.response.reason or f"Max preconditioning {state}."
|
|
210
|
+
formatter.rich.command_result(result.response.result, msg)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@climate_group.command("seat")
|
|
214
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
215
|
+
@click.argument("seat", type=click.Choice(list(SEAT_MAP)))
|
|
216
|
+
@click.argument("level", type=click.IntRange(0, 3))
|
|
217
|
+
@global_options
|
|
218
|
+
def seat_cmd(app_ctx: AppContext, vin_positional: str | None, seat: str, level: int) -> None:
|
|
219
|
+
"""Set seat heater for SEAT to LEVEL (0-3)."""
|
|
220
|
+
run_async(_cmd_seat(app_ctx, vin_positional, seat, level))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
async def _cmd_seat(
|
|
224
|
+
app_ctx: AppContext,
|
|
225
|
+
vin_positional: str | None,
|
|
226
|
+
seat: str,
|
|
227
|
+
level: int,
|
|
228
|
+
) -> None:
|
|
229
|
+
formatter = app_ctx.formatter
|
|
230
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
231
|
+
client, vehicle_api, cmd_api = get_command_api(app_ctx)
|
|
232
|
+
try:
|
|
233
|
+
result = await auto_wake(
|
|
234
|
+
formatter,
|
|
235
|
+
vehicle_api,
|
|
236
|
+
vin,
|
|
237
|
+
lambda: cmd_api.remote_seat_heater_request(
|
|
238
|
+
vin, seat_position=SEAT_MAP[seat], level=level
|
|
239
|
+
),
|
|
240
|
+
auto=app_ctx.auto_wake,
|
|
241
|
+
)
|
|
242
|
+
finally:
|
|
243
|
+
await client.close()
|
|
244
|
+
|
|
245
|
+
invalidate_cache_for_vin(app_ctx, vin)
|
|
246
|
+
|
|
247
|
+
if formatter.format == "json":
|
|
248
|
+
formatter.output(result, command="climate.seat")
|
|
249
|
+
else:
|
|
250
|
+
msg = result.response.reason or f"Seat heater set for {seat} (level {level})."
|
|
251
|
+
formatter.rich.command_result(result.response.result, msg)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@climate_group.command("seat-cool")
|
|
255
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
256
|
+
@click.argument("seat", type=click.Choice(list(SEAT_MAP)))
|
|
257
|
+
@click.argument("level", type=click.IntRange(0, 3))
|
|
258
|
+
@global_options
|
|
259
|
+
def seat_cool_cmd(app_ctx: AppContext, vin_positional: str | None, seat: str, level: int) -> None:
|
|
260
|
+
"""Set seat cooler for SEAT to LEVEL (0-3)."""
|
|
261
|
+
run_async(_cmd_seat_cool(app_ctx, vin_positional, seat, level))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
async def _cmd_seat_cool(
|
|
265
|
+
app_ctx: AppContext,
|
|
266
|
+
vin_positional: str | None,
|
|
267
|
+
seat: str,
|
|
268
|
+
level: int,
|
|
269
|
+
) -> None:
|
|
270
|
+
formatter = app_ctx.formatter
|
|
271
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
272
|
+
client, vehicle_api, cmd_api = get_command_api(app_ctx)
|
|
273
|
+
try:
|
|
274
|
+
result = await auto_wake(
|
|
275
|
+
formatter,
|
|
276
|
+
vehicle_api,
|
|
277
|
+
vin,
|
|
278
|
+
lambda: cmd_api.remote_seat_cooler_request(
|
|
279
|
+
vin, seat_position=SEAT_MAP[seat], seat_cooler_level=level
|
|
280
|
+
),
|
|
281
|
+
auto=app_ctx.auto_wake,
|
|
282
|
+
)
|
|
283
|
+
finally:
|
|
284
|
+
await client.close()
|
|
285
|
+
|
|
286
|
+
invalidate_cache_for_vin(app_ctx, vin)
|
|
287
|
+
|
|
288
|
+
if formatter.format == "json":
|
|
289
|
+
formatter.output(result, command="climate.seat-cool")
|
|
290
|
+
else:
|
|
291
|
+
msg = result.response.reason or f"Seat cooler set for {seat} (level {level})."
|
|
292
|
+
formatter.rich.command_result(result.response.result, msg)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@climate_group.command("wheel-heater")
|
|
296
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
297
|
+
@click.option("--on/--off", default=True, help="Enable or disable steering wheel heater")
|
|
298
|
+
@global_options
|
|
299
|
+
def wheel_heater_cmd(app_ctx: AppContext, vin_positional: str | None, on: bool) -> None:
|
|
300
|
+
"""Enable or disable steering wheel heater."""
|
|
301
|
+
run_async(_cmd_wheel_heater(app_ctx, vin_positional, on))
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
async def _cmd_wheel_heater(app_ctx: AppContext, vin_positional: str | None, on: bool) -> None:
|
|
305
|
+
formatter = app_ctx.formatter
|
|
306
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
307
|
+
client, vehicle_api, cmd_api = get_command_api(app_ctx)
|
|
308
|
+
try:
|
|
309
|
+
result = await auto_wake(
|
|
310
|
+
formatter,
|
|
311
|
+
vehicle_api,
|
|
312
|
+
vin,
|
|
313
|
+
lambda: cmd_api.remote_steering_wheel_heater_request(vin, on=on),
|
|
314
|
+
auto=app_ctx.auto_wake,
|
|
315
|
+
)
|
|
316
|
+
finally:
|
|
317
|
+
await client.close()
|
|
318
|
+
|
|
319
|
+
invalidate_cache_for_vin(app_ctx, vin)
|
|
320
|
+
|
|
321
|
+
if formatter.format == "json":
|
|
322
|
+
formatter.output(result, command="climate.wheel-heater")
|
|
323
|
+
else:
|
|
324
|
+
state = "enabled" if on else "disabled"
|
|
325
|
+
msg = result.response.reason or f"Steering wheel heater {state}."
|
|
326
|
+
formatter.rich.command_result(result.response.result, msg)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@climate_group.command("overheat")
|
|
330
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
331
|
+
@click.option("--on/--off", default=True, help="Enable or disable cabin overheat protection")
|
|
332
|
+
@click.option("--fan-only", is_flag=True, default=False, help="Use fan only (no AC)")
|
|
333
|
+
@global_options
|
|
334
|
+
def overheat_cmd(
|
|
335
|
+
app_ctx: AppContext, vin_positional: str | None, on: bool, fan_only: bool
|
|
336
|
+
) -> None:
|
|
337
|
+
"""Configure cabin overheat protection."""
|
|
338
|
+
run_async(_cmd_overheat(app_ctx, vin_positional, on, fan_only))
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
async def _cmd_overheat(
|
|
342
|
+
app_ctx: AppContext,
|
|
343
|
+
vin_positional: str | None,
|
|
344
|
+
on: bool,
|
|
345
|
+
fan_only: bool,
|
|
346
|
+
) -> None:
|
|
347
|
+
formatter = app_ctx.formatter
|
|
348
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
349
|
+
client, vehicle_api, cmd_api = get_command_api(app_ctx)
|
|
350
|
+
try:
|
|
351
|
+
result = await auto_wake(
|
|
352
|
+
formatter,
|
|
353
|
+
vehicle_api,
|
|
354
|
+
vin,
|
|
355
|
+
lambda: cmd_api.set_cabin_overheat_protection(vin, on=on, fan_only=fan_only),
|
|
356
|
+
auto=app_ctx.auto_wake,
|
|
357
|
+
)
|
|
358
|
+
finally:
|
|
359
|
+
await client.close()
|
|
360
|
+
|
|
361
|
+
invalidate_cache_for_vin(app_ctx, vin)
|
|
362
|
+
|
|
363
|
+
if formatter.format == "json":
|
|
364
|
+
formatter.output(result, command="climate.overheat")
|
|
365
|
+
else:
|
|
366
|
+
state = "enabled" if on else "disabled"
|
|
367
|
+
msg = result.response.reason or f"Cabin overheat protection {state}."
|
|
368
|
+
formatter.rich.command_result(result.response.result, msg)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@climate_group.command("bioweapon")
|
|
372
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
373
|
+
@click.option("--on/--off", default=True, help="Enable or disable bioweapon defense mode")
|
|
374
|
+
@click.option("--manual-override", is_flag=True, default=False, help="Force manual override")
|
|
375
|
+
@global_options
|
|
376
|
+
def bioweapon_cmd(
|
|
377
|
+
app_ctx: AppContext, vin_positional: str | None, on: bool, manual_override: bool
|
|
378
|
+
) -> None:
|
|
379
|
+
"""Enable or disable bioweapon defense mode."""
|
|
380
|
+
state = "enabled" if on else "disabled"
|
|
381
|
+
run_async(
|
|
382
|
+
execute_command(
|
|
383
|
+
app_ctx,
|
|
384
|
+
vin_positional,
|
|
385
|
+
"set_bioweapon_mode",
|
|
386
|
+
"climate.bioweapon",
|
|
387
|
+
body={"on": on, "manual_override": manual_override},
|
|
388
|
+
success_message=f"Bioweapon defense mode {state}.",
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@climate_group.command("cop-temp")
|
|
394
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
395
|
+
@click.argument("level", type=click.IntRange(0, 2))
|
|
396
|
+
@global_options
|
|
397
|
+
def cop_temp_cmd(app_ctx: AppContext, vin_positional: str | None, level: int) -> None:
|
|
398
|
+
"""Set cabin overheat protection temperature (0=low, 1=medium, 2=high)."""
|
|
399
|
+
run_async(
|
|
400
|
+
execute_command(
|
|
401
|
+
app_ctx,
|
|
402
|
+
vin_positional,
|
|
403
|
+
"set_cop_temp",
|
|
404
|
+
"climate.cop-temp",
|
|
405
|
+
body={"cop_temp": level},
|
|
406
|
+
success_message="Overheat protection temperature set.",
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@climate_group.command("auto-seat")
|
|
412
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
413
|
+
@click.argument("seat", type=click.Choice(list(SEAT_MAP)))
|
|
414
|
+
@click.option("--on/--off", default=True, help="Enable or disable auto seat climate")
|
|
415
|
+
@global_options
|
|
416
|
+
def auto_seat_cmd(app_ctx: AppContext, vin_positional: str | None, seat: str, on: bool) -> None:
|
|
417
|
+
"""Enable or disable auto seat climate for SEAT."""
|
|
418
|
+
state = "enabled" if on else "disabled"
|
|
419
|
+
run_async(
|
|
420
|
+
execute_command(
|
|
421
|
+
app_ctx,
|
|
422
|
+
vin_positional,
|
|
423
|
+
"remote_auto_seat_climate_request",
|
|
424
|
+
"climate.auto-seat",
|
|
425
|
+
body={"auto_seat_position": SEAT_MAP[seat], "auto_climate_on": on},
|
|
426
|
+
success_message=f"Auto seat climate {state} for {seat}.",
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@climate_group.command("auto-wheel")
|
|
432
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
433
|
+
@click.option("--on/--off", default=True, help="Enable or disable auto steering wheel heat")
|
|
434
|
+
@global_options
|
|
435
|
+
def auto_wheel_cmd(app_ctx: AppContext, vin_positional: str | None, on: bool) -> None:
|
|
436
|
+
"""Enable or disable auto steering wheel heat."""
|
|
437
|
+
state = "enabled" if on else "disabled"
|
|
438
|
+
run_async(
|
|
439
|
+
execute_command(
|
|
440
|
+
app_ctx,
|
|
441
|
+
vin_positional,
|
|
442
|
+
"remote_auto_steering_wheel_heat_climate_request",
|
|
443
|
+
"climate.auto-wheel",
|
|
444
|
+
body={"on": on},
|
|
445
|
+
success_message=f"Auto steering wheel heat {state}.",
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
@climate_group.command("wheel-level")
|
|
451
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
452
|
+
@click.argument("level", type=click.IntRange(0, 3))
|
|
453
|
+
@global_options
|
|
454
|
+
def wheel_level_cmd(app_ctx: AppContext, vin_positional: str | None, level: int) -> None:
|
|
455
|
+
"""Set steering wheel heat level (0=off, 1=low, 2=med, 3=high)."""
|
|
456
|
+
level_names = {0: "off", 1: "low", 2: "medium", 3: "high"}
|
|
457
|
+
run_async(
|
|
458
|
+
execute_command(
|
|
459
|
+
app_ctx,
|
|
460
|
+
vin_positional,
|
|
461
|
+
"remote_steering_wheel_heat_level_request",
|
|
462
|
+
"climate.wheel-level",
|
|
463
|
+
body={"level": level},
|
|
464
|
+
success_message=f"Steering wheel heat set to {level_names.get(level, level)}.",
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@climate_group.command("keeper")
|
|
470
|
+
@click.argument("vin_positional", required=False, default=None, metavar="VIN")
|
|
471
|
+
@click.argument("mode", type=click.Choice(list(KEEPER_MODE_MAP)))
|
|
472
|
+
@global_options
|
|
473
|
+
def keeper_cmd(app_ctx: AppContext, vin_positional: str | None, mode: str) -> None:
|
|
474
|
+
"""Set climate keeper mode (off/on/dog/camp)."""
|
|
475
|
+
run_async(_cmd_keeper(app_ctx, vin_positional, mode))
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
async def _cmd_keeper(app_ctx: AppContext, vin_positional: str | None, mode: str) -> None:
|
|
479
|
+
formatter = app_ctx.formatter
|
|
480
|
+
vin = require_vin(vin_positional, app_ctx.vin)
|
|
481
|
+
client, vehicle_api, cmd_api = get_command_api(app_ctx)
|
|
482
|
+
try:
|
|
483
|
+
result = await auto_wake(
|
|
484
|
+
formatter,
|
|
485
|
+
vehicle_api,
|
|
486
|
+
vin,
|
|
487
|
+
lambda: cmd_api.set_climate_keeper_mode(
|
|
488
|
+
vin, climate_keeper_mode=KEEPER_MODE_MAP[mode]
|
|
489
|
+
),
|
|
490
|
+
auto=app_ctx.auto_wake,
|
|
491
|
+
)
|
|
492
|
+
finally:
|
|
493
|
+
await client.close()
|
|
494
|
+
|
|
495
|
+
invalidate_cache_for_vin(app_ctx, vin)
|
|
496
|
+
|
|
497
|
+
if formatter.format == "json":
|
|
498
|
+
formatter.output(result, command="climate.keeper")
|
|
499
|
+
else:
|
|
500
|
+
msg = result.response.reason or f"Climate keeper mode set to {mode}."
|
|
501
|
+
formatter.rich.command_result(result.response.result, msg)
|