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
@@ -0,0 +1,621 @@
1
+ """Protobuf payload builders for the Vehicle Command Protocol.
2
+
3
+ Builds the ``protobuf_message_as_bytes`` content for each command.
4
+ VCSEC commands produce a serialized ``UnsignedMessage``.
5
+ Infotainment commands produce a serialized ``Action { VehicleAction { ... } }``.
6
+
7
+ Field numbers match Tesla's vehicle-command proto definitions:
8
+ https://github.com/teslamotors/vehicle-command/tree/main/pkg/protocol/protobuf
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Callable
14
+ from typing import Any
15
+
16
+ from tescmd.protocol.protobuf.messages import (
17
+ _encode_length_delimited,
18
+ _encode_varint_field,
19
+ )
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Low-level helpers
23
+ # ---------------------------------------------------------------------------
24
+
25
+ _VOID = b"" # Empty protobuf message (Void type)
26
+
27
+
28
+ def _wrap_vehicle_action(field_number: int, inner: bytes) -> bytes:
29
+ """Wrap an inner action in VehicleAction → Action protobuf."""
30
+ # VehicleAction { specificAction: inner }
31
+ vehicle_action = _encode_length_delimited(field_number, inner)
32
+ # Action { vehicleAction(field 2): vehicle_action }
33
+ return _encode_length_delimited(2, vehicle_action)
34
+
35
+
36
+ def _void_vehicle_action(field_number: int) -> bytes:
37
+ """Build an Action wrapping a VehicleAction with a Void field."""
38
+ return _wrap_vehicle_action(field_number, _VOID)
39
+
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # VCSEC payload builders
43
+ # ---------------------------------------------------------------------------
44
+
45
+ # UnsignedMessage field numbers (from vcsec.proto)
46
+ _VCSEC_RKE_ACTION = 2 # varint — RKEAction_E enum
47
+ _VCSEC_CLOSURE_MOVE_REQUEST = 4 # submessage — ClosureMoveRequest
48
+
49
+ # RKEAction_E enum values
50
+ _RKE_UNLOCK = 0
51
+ _RKE_LOCK = 1
52
+ _RKE_REMOTE_DRIVE = 20
53
+ _RKE_AUTO_SECURE = 29
54
+ _RKE_WAKE = 30
55
+
56
+ # ClosureMoveType_E enum values
57
+ _CLOSURE_NONE = 0
58
+ _CLOSURE_MOVE = 1
59
+ _CLOSURE_STOP = 2
60
+ _CLOSURE_OPEN = 3
61
+ _CLOSURE_CLOSE = 4
62
+
63
+ # ClosureMoveRequest field numbers
64
+ _CLOSURE_REAR_TRUNK = 5
65
+ _CLOSURE_FRONT_TRUNK = 6
66
+ _CLOSURE_CHARGE_PORT = 7
67
+
68
+
69
+ def _vcsec_rke(action: int) -> bytes:
70
+ """Build UnsignedMessage with RKEAction enum."""
71
+ return _encode_varint_field(_VCSEC_RKE_ACTION, action)
72
+
73
+
74
+ def _build_trunk_payload(body: dict[str, Any]) -> bytes:
75
+ """Build VCSEC ClosureMoveRequest for trunk/frunk."""
76
+ is_front = body.get("which_trunk") == "front"
77
+ field = _CLOSURE_FRONT_TRUNK if is_front else _CLOSURE_REAR_TRUNK
78
+ return _vcsec_closure_move(**{str(field): _CLOSURE_MOVE})
79
+
80
+
81
+ def _vcsec_closure_move(**fields: int) -> bytes:
82
+ """Build UnsignedMessage with ClosureMoveRequest.
83
+
84
+ Each keyword arg maps a field number to a ClosureMoveType_E value.
85
+ """
86
+ inner = b""
87
+ for field_num, move_type in fields.items():
88
+ inner += _encode_varint_field(int(field_num), move_type)
89
+ return _encode_length_delimited(_VCSEC_CLOSURE_MOVE_REQUEST, inner)
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Infotainment (CarServer) VehicleAction field numbers
94
+ # ---------------------------------------------------------------------------
95
+
96
+ # From car_server.proto VehicleAction oneof
97
+ _VA_CHARGING_SET_LIMIT = 5
98
+ _VA_CHARGING_START_STOP = 6
99
+ _VA_DRIVING_CLEAR_SPEED_LIMIT_PIN = 7
100
+ _VA_DRIVING_SET_SPEED_LIMIT = 8
101
+ _VA_DRIVING_SPEED_LIMIT = 9
102
+ _VA_HVAC_AUTO = 10
103
+ _VA_HVAC_PRECONDITIONING_MAX = 12
104
+ _VA_HVAC_STEERING_WHEEL_HEATER = 13
105
+ _VA_HVAC_TEMP_ADJUSTMENT = 14
106
+ _VA_MEDIA_PLAY = 15
107
+ _VA_MEDIA_UPDATE_VOLUME = 16
108
+ _VA_MEDIA_NEXT_FAV = 17
109
+ _VA_MEDIA_PREV_FAV = 18
110
+ _VA_MEDIA_NEXT_TRACK = 19
111
+ _VA_MEDIA_PREV_TRACK = 20
112
+ _VA_CANCEL_SOFTWARE_UPDATE = 25
113
+ _VA_FLASH_LIGHTS = 26
114
+ _VA_HONK_HORN = 27
115
+ _VA_RESET_VALET_PIN = 28
116
+ _VA_SCHEDULE_SOFTWARE_UPDATE = 29
117
+ _VA_SET_SENTRY_MODE = 30
118
+ _VA_SET_VALET_MODE = 31
119
+ _VA_SUNROOF = 32
120
+ _VA_TRIGGER_HOMELINK = 33
121
+ _VA_WINDOW_CONTROL = 34
122
+ _VA_BIOWEAPON_MODE = 35
123
+ _VA_HVAC_SEAT_HEATER = 36
124
+ _VA_SCHEDULED_CHARGING = 41
125
+ _VA_SCHEDULED_DEPARTURE = 42
126
+ _VA_SET_CHARGING_AMPS = 43
127
+ _VA_CLIMATE_KEEPER = 44
128
+ _VA_AUTO_SEAT_CLIMATE = 48
129
+ _VA_HVAC_SEAT_COOLER = 49
130
+ _VA_SET_COP = 50
131
+ _VA_SET_VEHICLE_NAME = 54
132
+ _VA_CHARGE_PORT_CLOSE = 61
133
+ _VA_CHARGE_PORT_OPEN = 62
134
+ _VA_GUEST_MODE = 65
135
+ _VA_SET_COP_TEMP = 66
136
+ _VA_ERASE_USER_DATA = 72
137
+ _VA_SET_PIN_TO_DRIVE = 77
138
+ _VA_RESET_PIN_TO_DRIVE = 78
139
+ _VA_CLEAR_SPEED_LIMIT_PIN_ADMIN = 79
140
+ _VA_ADD_CHARGE_SCHEDULE = 97
141
+ _VA_REMOVE_CHARGE_SCHEDULE = 98
142
+ _VA_ADD_PRECONDITION_SCHEDULE = 99
143
+ _VA_REMOVE_PRECONDITION_SCHEDULE = 100
144
+ _VA_BATCH_REMOVE_PRECONDITION = 107
145
+ _VA_BATCH_REMOVE_CHARGE = 108
146
+
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # Infotainment payload builders
150
+ # ---------------------------------------------------------------------------
151
+
152
+
153
+ def _charging_start_stop(body: dict[str, Any]) -> bytes:
154
+ """ChargingStartStopAction: oneof { start=2, stop=5 }."""
155
+ # The REST API command name determines start vs stop, not the body
156
+ # This builder is called with the body dict which may be empty
157
+ # The command name mapping happens in the BUILDERS dict below
158
+ raise AssertionError("Use _charging_start or _charging_stop directly")
159
+
160
+
161
+ def _charging_start(_body: dict[str, Any]) -> bytes:
162
+ inner = _encode_length_delimited(2, _VOID) # start field = 2
163
+ return _wrap_vehicle_action(_VA_CHARGING_START_STOP, inner)
164
+
165
+
166
+ def _charging_stop(_body: dict[str, Any]) -> bytes:
167
+ inner = _encode_length_delimited(5, _VOID) # stop field = 5
168
+ return _wrap_vehicle_action(_VA_CHARGING_START_STOP, inner)
169
+
170
+
171
+ def _charging_standard(_body: dict[str, Any]) -> bytes:
172
+ inner = _encode_length_delimited(3, _VOID) # start_standard = 3
173
+ return _wrap_vehicle_action(_VA_CHARGING_START_STOP, inner)
174
+
175
+
176
+ def _charging_max_range(_body: dict[str, Any]) -> bytes:
177
+ inner = _encode_length_delimited(4, _VOID) # start_max_range = 4
178
+ return _wrap_vehicle_action(_VA_CHARGING_START_STOP, inner)
179
+
180
+
181
+ def _set_charge_limit(body: dict[str, Any]) -> bytes:
182
+ """ChargingSetLimitAction: percent (field 1)."""
183
+ percent = body.get("percent", 80)
184
+ inner = _encode_varint_field(1, int(percent))
185
+ return _wrap_vehicle_action(_VA_CHARGING_SET_LIMIT, inner)
186
+
187
+
188
+ def _set_charging_amps(body: dict[str, Any]) -> bytes:
189
+ """SetChargingAmpsAction: charging_amps (field 1)."""
190
+ amps = body.get("charging_amps", 32)
191
+ inner = _encode_varint_field(1, int(amps))
192
+ return _wrap_vehicle_action(_VA_SET_CHARGING_AMPS, inner)
193
+
194
+
195
+ def _hvac_auto(body: dict[str, Any]) -> bytes:
196
+ """HvacAutoAction: power_on (field 1)."""
197
+ on = body.get("power_on", True)
198
+ inner = _encode_varint_field(1, 1 if on else 0)
199
+ return _wrap_vehicle_action(_VA_HVAC_AUTO, inner)
200
+
201
+
202
+ def _hvac_auto_start(_body: dict[str, Any]) -> bytes:
203
+ inner = _encode_varint_field(1, 1) # power_on = true
204
+ return _wrap_vehicle_action(_VA_HVAC_AUTO, inner)
205
+
206
+
207
+ def _hvac_auto_stop(_body: dict[str, Any]) -> bytes:
208
+ inner = _encode_varint_field(1, 0) # power_on = false
209
+ return _wrap_vehicle_action(_VA_HVAC_AUTO, inner)
210
+
211
+
212
+ def _set_temps(body: dict[str, Any]) -> bytes:
213
+ """HvacTemperatureAdjustmentAction: driver (field 6), passenger (field 7)."""
214
+ inner = b""
215
+ if "driver_temp" in body:
216
+ # Float → fixed32 (wire type 5) is complex; use varint with *10 encoding
217
+ # Actually Tesla uses float fields. For protobuf float, we need wire type 5.
218
+ import struct
219
+
220
+ driver = float(body["driver_temp"])
221
+ inner += _encode_tag_raw(6, 5) + struct.pack("<f", driver)
222
+ if "passenger_temp" in body:
223
+ import struct
224
+
225
+ passenger = float(body["passenger_temp"])
226
+ inner += _encode_tag_raw(7, 5) + struct.pack("<f", passenger)
227
+ return _wrap_vehicle_action(_VA_HVAC_TEMP_ADJUSTMENT, inner)
228
+
229
+
230
+ def _encode_tag_raw(field_number: int, wire_type: int) -> bytes:
231
+ """Encode a protobuf field tag (re-export for float fields)."""
232
+ from tescmd.protocol.protobuf.messages import _encode_tag
233
+
234
+ return _encode_tag(field_number, wire_type)
235
+
236
+
237
+ def _set_preconditioning_max(body: dict[str, Any]) -> bytes:
238
+ """HvacSetPreconditioningMaxAction: on (field 1), manual_override (field 2)."""
239
+ on = body.get("on", True)
240
+ inner = _encode_varint_field(1, 1 if on else 0)
241
+ if body.get("manual_override"):
242
+ inner += _encode_varint_field(2, 1)
243
+ return _wrap_vehicle_action(_VA_HVAC_PRECONDITIONING_MAX, inner)
244
+
245
+
246
+ def _seat_heater(body: dict[str, Any]) -> bytes:
247
+ """HvacSeatHeaterActions: hvacSeatHeaterAction (field 1, repeated submessage).
248
+
249
+ Each sub-message has: seat_position (field 1), seat_heater_level (field 2).
250
+ """
251
+ seat_msg = b""
252
+ seat_position = body.get("seat_heater", body.get("seat_position", 0))
253
+ level = body.get("seat_heater_level", body.get("level", 0))
254
+ seat_msg = _encode_varint_field(1, int(seat_position))
255
+ seat_msg += _encode_varint_field(2, int(level))
256
+ inner = _encode_length_delimited(1, seat_msg)
257
+ return _wrap_vehicle_action(_VA_HVAC_SEAT_HEATER, inner)
258
+
259
+
260
+ def _seat_cooler(body: dict[str, Any]) -> bytes:
261
+ """HvacSeatCoolerActions: hvacSeatCoolerAction (field 1)."""
262
+ seat_msg = b""
263
+ seat_position = body.get("seat_position", 0)
264
+ level = body.get("seat_cooler_level", body.get("level", 0))
265
+ seat_msg = _encode_varint_field(1, int(seat_position))
266
+ seat_msg += _encode_varint_field(2, int(level))
267
+ inner = _encode_length_delimited(1, seat_msg)
268
+ return _wrap_vehicle_action(_VA_HVAC_SEAT_COOLER, inner)
269
+
270
+
271
+ def _steering_wheel_heater(body: dict[str, Any]) -> bytes:
272
+ """HvacSteeringWheelHeaterAction: power_on (field 1)."""
273
+ on = body.get("on", True)
274
+ inner = _encode_varint_field(1, 1 if on else 0)
275
+ return _wrap_vehicle_action(_VA_HVAC_STEERING_WHEEL_HEATER, inner)
276
+
277
+
278
+ def _set_sentry_mode(body: dict[str, Any]) -> bytes:
279
+ """VehicleControlSetSentryModeAction: on (field 1)."""
280
+ on = body.get("on", True)
281
+ inner = _encode_varint_field(1, 1 if on else 0)
282
+ return _wrap_vehicle_action(_VA_SET_SENTRY_MODE, inner)
283
+
284
+
285
+ def _set_valet_mode(body: dict[str, Any]) -> bytes:
286
+ """VehicleControlSetValetModeAction: on (field 1), password (field 2)."""
287
+ on = body.get("on", True)
288
+ inner = _encode_varint_field(1, 1 if on else 0)
289
+ if "password" in body:
290
+ inner += _encode_length_delimited(2, str(body["password"]).encode())
291
+ return _wrap_vehicle_action(_VA_SET_VALET_MODE, inner)
292
+
293
+
294
+ def _set_pin_to_drive(body: dict[str, Any]) -> bytes:
295
+ """VehicleControlSetPinToDriveAction: on (field 1), password (field 2)."""
296
+ on = body.get("on", True)
297
+ inner = _encode_varint_field(1, 1 if on else 0)
298
+ if "password" in body:
299
+ inner += _encode_length_delimited(2, str(body["password"]).encode())
300
+ return _wrap_vehicle_action(_VA_SET_PIN_TO_DRIVE, inner)
301
+
302
+
303
+ def _speed_limit_activate(body: dict[str, Any]) -> bytes:
304
+ """DrivingSpeedLimitAction: activate (field 1) = true, pin (field 2)."""
305
+ inner = _encode_varint_field(1, 1)
306
+ if "pin" in body:
307
+ inner += _encode_length_delimited(2, str(body["pin"]).encode())
308
+ return _wrap_vehicle_action(_VA_DRIVING_SPEED_LIMIT, inner)
309
+
310
+
311
+ def _speed_limit_deactivate(body: dict[str, Any]) -> bytes:
312
+ """DrivingSpeedLimitAction: activate (field 1) = false, pin (field 2)."""
313
+ inner = _encode_varint_field(1, 0)
314
+ if "pin" in body:
315
+ inner += _encode_length_delimited(2, str(body["pin"]).encode())
316
+ return _wrap_vehicle_action(_VA_DRIVING_SPEED_LIMIT, inner)
317
+
318
+
319
+ def _speed_limit_clear_pin(body: dict[str, Any]) -> bytes:
320
+ """DrivingClearSpeedLimitPinAction: pin (field 1)."""
321
+ pin = body.get("pin", "")
322
+ inner = _encode_length_delimited(1, str(pin).encode()) if pin else _VOID
323
+ return _wrap_vehicle_action(_VA_DRIVING_CLEAR_SPEED_LIMIT_PIN, inner)
324
+
325
+
326
+ def _speed_limit_set(body: dict[str, Any]) -> bytes:
327
+ """DrivingSetSpeedLimitAction: limit_mph (field 1)."""
328
+ limit = body.get("limit_mph", 65)
329
+ inner = _encode_varint_field(1, int(limit))
330
+ return _wrap_vehicle_action(_VA_DRIVING_SET_SPEED_LIMIT, inner)
331
+
332
+
333
+ def _media_volume(body: dict[str, Any]) -> bytes:
334
+ """MediaUpdateVolume: volume_delta (field 1) or volume_absolute_float (field 3)."""
335
+ inner = b""
336
+ if "volume" in body:
337
+ import struct
338
+
339
+ vol = float(body["volume"])
340
+ inner = _encode_tag_raw(3, 5) + struct.pack("<f", vol)
341
+ elif "volume_delta" in body:
342
+ import struct
343
+
344
+ delta = float(body["volume_delta"])
345
+ inner = _encode_tag_raw(1, 5) + struct.pack("<f", delta)
346
+ return _wrap_vehicle_action(_VA_MEDIA_UPDATE_VOLUME, inner)
347
+
348
+
349
+ def _schedule_software_update(body: dict[str, Any]) -> bytes:
350
+ """ScheduleSoftwareUpdateAction: offset_sec (field 1)."""
351
+ offset = body.get("offset_sec", 0)
352
+ inner = _encode_varint_field(1, int(offset))
353
+ return _wrap_vehicle_action(_VA_SCHEDULE_SOFTWARE_UPDATE, inner)
354
+
355
+
356
+ def _sunroof(body: dict[str, Any]) -> bytes:
357
+ """VehicleControlSunroofOpenCloseAction."""
358
+ state = body.get("state", "")
359
+ inner = b""
360
+ if state == "vent":
361
+ inner = _encode_varint_field(3, 1) # vent = true
362
+ elif state == "close":
363
+ inner = _encode_varint_field(4, 1) # close = true
364
+ elif state == "open":
365
+ inner = _encode_varint_field(5, 1) # open = true
366
+ return _wrap_vehicle_action(_VA_SUNROOF, inner)
367
+
368
+
369
+ def _trigger_homelink(body: dict[str, Any]) -> bytes:
370
+ """VehicleControlTriggerHomelinkAction: lat/lon in LatLong submessage (field 1)."""
371
+ lat = body.get("lat", 0.0)
372
+ lon = body.get("lon", 0.0)
373
+ import struct
374
+
375
+ # LatLong message: latitude (field 1, float), longitude (field 2, float)
376
+ location = _encode_tag_raw(1, 5) + struct.pack("<f", float(lat))
377
+ location += _encode_tag_raw(2, 5) + struct.pack("<f", float(lon))
378
+ inner = _encode_length_delimited(1, location)
379
+ return _wrap_vehicle_action(_VA_TRIGGER_HOMELINK, inner)
380
+
381
+
382
+ def _set_cabin_overheat_protection(body: dict[str, Any]) -> bytes:
383
+ """SetCabinOverheatProtectionAction: on (field 1), fan_only (field 2)."""
384
+ on = body.get("on", True)
385
+ inner = _encode_varint_field(1, 1 if on else 0)
386
+ if body.get("fan_only"):
387
+ inner += _encode_varint_field(2, 1)
388
+ return _wrap_vehicle_action(_VA_SET_COP, inner)
389
+
390
+
391
+ def _set_climate_keeper(body: dict[str, Any]) -> bytes:
392
+ """HvacClimateKeeperAction: ClimateKeeperAction (field 1), manual_override (field 2)."""
393
+ action = body.get("climate_keeper_mode", 0)
394
+ inner = _encode_varint_field(1, int(action))
395
+ if body.get("manual_override"):
396
+ inner += _encode_varint_field(2, 1)
397
+ return _wrap_vehicle_action(_VA_CLIMATE_KEEPER, inner)
398
+
399
+
400
+ def _set_cop_temp(body: dict[str, Any]) -> bytes:
401
+ """SetCopTempAction: copActivationTemp (field 1)."""
402
+ temp = body.get("cop_temp", 0)
403
+ inner = _encode_varint_field(1, int(temp))
404
+ return _wrap_vehicle_action(_VA_SET_COP_TEMP, inner)
405
+
406
+
407
+ def _auto_seat_climate(body: dict[str, Any]) -> bytes:
408
+ """AutoSeatClimateAction: carseat (field 1, repeated submessage)."""
409
+ seat_msg = b""
410
+ seat = body.get("auto_seat_position", body.get("seat_position", 0))
411
+ on = body.get("on", True)
412
+ seat_msg = _encode_varint_field(1, int(seat))
413
+ seat_msg += _encode_varint_field(2, 1 if on else 0)
414
+ inner = _encode_length_delimited(1, seat_msg)
415
+ return _wrap_vehicle_action(_VA_AUTO_SEAT_CLIMATE, inner)
416
+
417
+
418
+ def _bioweapon_mode(body: dict[str, Any]) -> bytes:
419
+ """HvacBioweaponModeAction: on (field 1), manual_override (field 2)."""
420
+ on = body.get("on", True)
421
+ inner = _encode_varint_field(1, 1 if on else 0)
422
+ if body.get("manual_override"):
423
+ inner += _encode_varint_field(2, 1)
424
+ return _wrap_vehicle_action(_VA_BIOWEAPON_MODE, inner)
425
+
426
+
427
+ def _window_control(body: dict[str, Any]) -> bytes:
428
+ """VehicleControlWindowAction — vent or close."""
429
+ command = body.get("command", "vent")
430
+ field = 2 if command == "close" else 1
431
+ inner = _encode_varint_field(field, 1)
432
+ # lat/lon may be required for window control
433
+ if "lat" in body and "lon" in body:
434
+ import struct
435
+
436
+ location = _encode_tag_raw(3, 5) + struct.pack("<f", float(body["lat"]))
437
+ location += _encode_tag_raw(4, 5) + struct.pack("<f", float(body["lon"]))
438
+ inner += location
439
+ return _wrap_vehicle_action(_VA_WINDOW_CONTROL, inner)
440
+
441
+
442
+ def _set_vehicle_name(body: dict[str, Any]) -> bytes:
443
+ """SetVehicleNameAction: vehicleName (field 1, string)."""
444
+ name = body.get("vehicle_name", "")
445
+ inner = _encode_length_delimited(1, str(name).encode())
446
+ return _wrap_vehicle_action(_VA_SET_VEHICLE_NAME, inner)
447
+
448
+
449
+ def _guest_mode(body: dict[str, Any]) -> bytes:
450
+ """GuestModeAction: enable (field 1)."""
451
+ on = body.get("enable", True)
452
+ inner = _encode_varint_field(1, 1 if on else 0)
453
+ return _wrap_vehicle_action(_VA_GUEST_MODE, inner)
454
+
455
+
456
+ def _erase_user_data(_body: dict[str, Any]) -> bytes:
457
+ """EraseUserDataAction: reason (field 1, string)."""
458
+ return _void_vehicle_action(_VA_ERASE_USER_DATA)
459
+
460
+
461
+ def _scheduled_charging(body: dict[str, Any]) -> bytes:
462
+ """ScheduledChargingAction."""
463
+ inner = b""
464
+ if "enable" in body:
465
+ inner += _encode_varint_field(1, 1 if body["enable"] else 0)
466
+ if "charging_time" in body:
467
+ inner += _encode_varint_field(2, int(body["charging_time"]))
468
+ return _wrap_vehicle_action(_VA_SCHEDULED_CHARGING, inner)
469
+
470
+
471
+ def _scheduled_departure(body: dict[str, Any]) -> bytes:
472
+ """ScheduledDepartureAction."""
473
+ inner = b""
474
+ if "enable" in body:
475
+ inner += _encode_varint_field(1, 1 if body["enable"] else 0)
476
+ if "departure_time" in body:
477
+ inner += _encode_varint_field(2, int(body["departure_time"]))
478
+ return _wrap_vehicle_action(_VA_SCHEDULED_DEPARTURE, inner)
479
+
480
+
481
+ def _add_charge_schedule(body: dict[str, Any]) -> bytes:
482
+ """AddChargeScheduleAction — pass through body fields."""
483
+ inner = b""
484
+ # Schedule ID, start time, end time, etc. are encoded as varint fields
485
+ for key, field_num in [("id", 1), ("start_time", 2), ("end_time", 3)]:
486
+ if key in body:
487
+ inner += _encode_varint_field(field_num, int(body[key]))
488
+ return _wrap_vehicle_action(_VA_ADD_CHARGE_SCHEDULE, inner)
489
+
490
+
491
+ def _remove_charge_schedule(body: dict[str, Any]) -> bytes:
492
+ """RemoveChargeScheduleAction: id (field 1)."""
493
+ schedule_id = body.get("id", 0)
494
+ inner = _encode_varint_field(1, int(schedule_id))
495
+ return _wrap_vehicle_action(_VA_REMOVE_CHARGE_SCHEDULE, inner)
496
+
497
+
498
+ def _add_precondition_schedule(body: dict[str, Any]) -> bytes:
499
+ """AddPreconditionScheduleAction."""
500
+ inner = b""
501
+ for key, field_num in [("id", 1), ("start_time", 2), ("end_time", 3)]:
502
+ if key in body:
503
+ inner += _encode_varint_field(field_num, int(body[key]))
504
+ return _wrap_vehicle_action(_VA_ADD_PRECONDITION_SCHEDULE, inner)
505
+
506
+
507
+ def _remove_precondition_schedule(body: dict[str, Any]) -> bytes:
508
+ """RemovePreconditionScheduleAction: id (field 1)."""
509
+ schedule_id = body.get("id", 0)
510
+ inner = _encode_varint_field(1, int(schedule_id))
511
+ return _wrap_vehicle_action(_VA_REMOVE_PRECONDITION_SCHEDULE, inner)
512
+
513
+
514
+ def _steering_wheel_heat_level(body: dict[str, Any]) -> bytes:
515
+ """HvacSteeringWheelHeaterAction with level."""
516
+ level = body.get("level", 0)
517
+ inner = _encode_varint_field(1, int(level))
518
+ return _wrap_vehicle_action(_VA_HVAC_STEERING_WHEEL_HEATER, inner)
519
+
520
+
521
+ # ---------------------------------------------------------------------------
522
+ # Builder registry — maps REST command names to payload builder functions
523
+ # ---------------------------------------------------------------------------
524
+
525
+ _PayloadBuilder = Callable[[dict[str, Any]], bytes]
526
+
527
+ _BUILDERS: dict[str, _PayloadBuilder] = {
528
+ # VCSEC commands → UnsignedMessage payloads
529
+ "door_lock": lambda _: _vcsec_rke(_RKE_LOCK),
530
+ "door_unlock": lambda _: _vcsec_rke(_RKE_UNLOCK),
531
+ "remote_start_drive": lambda _: _vcsec_rke(_RKE_REMOTE_DRIVE),
532
+ "actuate_trunk": lambda body: _build_trunk_payload(body),
533
+ # Infotainment commands → Action { VehicleAction } payloads
534
+ "charge_start": _charging_start,
535
+ "charge_stop": _charging_stop,
536
+ "charge_standard": _charging_standard,
537
+ "charge_max_range": _charging_max_range,
538
+ "charge_port_door_open": lambda _: _void_vehicle_action(_VA_CHARGE_PORT_OPEN),
539
+ "charge_port_door_close": lambda _: _void_vehicle_action(_VA_CHARGE_PORT_CLOSE),
540
+ "set_charge_limit": _set_charge_limit,
541
+ "set_charging_amps": _set_charging_amps,
542
+ "set_scheduled_charging": _scheduled_charging,
543
+ "set_scheduled_departure": _scheduled_departure,
544
+ "add_charge_schedule": _add_charge_schedule,
545
+ "remove_charge_schedule": _remove_charge_schedule,
546
+ "add_precondition_schedule": _add_precondition_schedule,
547
+ "remove_precondition_schedule": _remove_precondition_schedule,
548
+ # Climate
549
+ "auto_conditioning_start": _hvac_auto_start,
550
+ "auto_conditioning_stop": _hvac_auto_stop,
551
+ "set_temps": _set_temps,
552
+ "set_preconditioning_max": _set_preconditioning_max,
553
+ "remote_seat_heater_request": _seat_heater,
554
+ "remote_seat_cooler_request": _seat_cooler,
555
+ "remote_steering_wheel_heater_request": _steering_wheel_heater,
556
+ "set_cabin_overheat_protection": _set_cabin_overheat_protection,
557
+ "set_climate_keeper_mode": _set_climate_keeper,
558
+ "set_cop_temp": _set_cop_temp,
559
+ "remote_auto_seat_climate_request": _auto_seat_climate,
560
+ "remote_auto_steering_wheel_heat_climate_request": _steering_wheel_heater,
561
+ "remote_steering_wheel_heat_level_request": _steering_wheel_heat_level,
562
+ "set_bioweapon_mode": _bioweapon_mode,
563
+ # Security (infotainment-routed)
564
+ "set_sentry_mode": _set_sentry_mode,
565
+ "set_valet_mode": _set_valet_mode,
566
+ "reset_valet_pin": lambda _: _void_vehicle_action(_VA_RESET_VALET_PIN),
567
+ "speed_limit_activate": _speed_limit_activate,
568
+ "speed_limit_deactivate": _speed_limit_deactivate,
569
+ "speed_limit_set_limit": _speed_limit_set,
570
+ "speed_limit_clear_pin": lambda body: _speed_limit_clear_pin(body),
571
+ "reset_pin_to_drive_pin": lambda _: _void_vehicle_action(_VA_RESET_PIN_TO_DRIVE),
572
+ "clear_pin_to_drive_admin": lambda _: _void_vehicle_action(_VA_RESET_PIN_TO_DRIVE),
573
+ "speed_limit_clear_pin_admin": lambda _: _void_vehicle_action(_VA_CLEAR_SPEED_LIMIT_PIN_ADMIN),
574
+ "honk_horn": lambda _: _void_vehicle_action(_VA_HONK_HORN),
575
+ "flash_lights": lambda _: _void_vehicle_action(_VA_FLASH_LIGHTS),
576
+ "set_pin_to_drive": _set_pin_to_drive,
577
+ "guest_mode": _guest_mode,
578
+ "erase_user_data": _erase_user_data,
579
+ "remote_boombox": lambda _: _void_vehicle_action(_VA_HONK_HORN), # Maps to honk
580
+ # Media
581
+ "media_toggle_playback": lambda _: _void_vehicle_action(_VA_MEDIA_PLAY),
582
+ "media_next_track": lambda _: _void_vehicle_action(_VA_MEDIA_NEXT_TRACK),
583
+ "media_prev_track": lambda _: _void_vehicle_action(_VA_MEDIA_PREV_TRACK),
584
+ "media_next_fav": lambda _: _void_vehicle_action(_VA_MEDIA_NEXT_FAV),
585
+ "media_prev_fav": lambda _: _void_vehicle_action(_VA_MEDIA_PREV_FAV),
586
+ "media_volume_up": lambda body: _media_volume(
587
+ {"volume_delta": body.get("volume_delta", 0.5)},
588
+ ),
589
+ "media_volume_down": lambda body: _media_volume(
590
+ {"volume_delta": body.get("volume_delta", -0.5)},
591
+ ),
592
+ "adjust_volume": _media_volume,
593
+ # Navigation
594
+ "trigger_homelink": _trigger_homelink,
595
+ # Software
596
+ "schedule_software_update": _schedule_software_update,
597
+ "cancel_software_update": lambda _: _void_vehicle_action(_VA_CANCEL_SOFTWARE_UPDATE),
598
+ # Vehicle
599
+ "set_vehicle_name": _set_vehicle_name,
600
+ # Sunroof
601
+ "sun_roof_control": _sunroof,
602
+ # Window (infotainment path)
603
+ "window_control": _window_control,
604
+ }
605
+
606
+
607
+ def build_command_payload(command: str, body: dict[str, Any] | None) -> bytes:
608
+ """Build the protobuf payload for a vehicle command.
609
+
610
+ Returns serialized bytes suitable for ``protobuf_message_as_bytes``
611
+ in a :class:`RoutableMessage`.
612
+
613
+ Raises :class:`ValueError` if no builder exists for the command.
614
+ """
615
+ builder = _BUILDERS.get(command)
616
+ if builder is None:
617
+ raise ValueError(
618
+ f"No protobuf payload builder for command '{command}'. "
619
+ "This command may not be supported via the Vehicle Command Protocol."
620
+ )
621
+ return builder(body or {})
@@ -0,0 +1,6 @@
1
+ """Vendored protobuf message definitions for Tesla Vehicle Command Protocol.
2
+
3
+ These are minimal, hand-written message definitions that are wire-compatible
4
+ with Tesla's published `.proto` schemas. Generated ``_pb2.py`` files can
5
+ replace them when full ``protoc`` generation is wired up.
6
+ """