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
|
@@ -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
|
+
"""
|