tesla-fleet-api 1.0.0__py3-none-any.whl → 1.0.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 (49) hide show
  1. tesla_fleet_api/__init__.py +5 -7
  2. tesla_fleet_api/const.py +1 -1
  3. tesla_fleet_api/exceptions.py +16 -2
  4. tesla_fleet_api/tesla/__init__.py +3 -3
  5. tesla_fleet_api/tesla/bluetooth.py +13 -3
  6. tesla_fleet_api/tesla/charging.py +1 -1
  7. tesla_fleet_api/tesla/energysite.py +2 -2
  8. tesla_fleet_api/tesla/fleet.py +8 -8
  9. tesla_fleet_api/tesla/oauth.py +2 -2
  10. tesla_fleet_api/tesla/user.py +1 -1
  11. tesla_fleet_api/tesla/vehicle/__init__.py +13 -0
  12. tesla_fleet_api/tesla/vehicle/bluetooth.py +226 -0
  13. tesla_fleet_api/tesla/vehicle/commands.py +1284 -0
  14. tesla_fleet_api/tesla/vehicle/fleet.py +847 -0
  15. tesla_fleet_api/tesla/vehicle/proto/__init__.py +0 -0
  16. tesla_fleet_api/tesla/vehicle/proto/__init__.pyi +9 -0
  17. tesla_fleet_api/tesla/vehicle/proto/car_server_pb2.py +175 -0
  18. tesla_fleet_api/tesla/vehicle/proto/car_server_pb2.pyi +904 -0
  19. tesla_fleet_api/tesla/vehicle/proto/common_pb2.py +33 -0
  20. tesla_fleet_api/tesla/vehicle/proto/common_pb2.pyi +130 -0
  21. tesla_fleet_api/tesla/vehicle/proto/errors_pb2.py +17 -0
  22. tesla_fleet_api/tesla/vehicle/proto/errors_pb2.pyi +32 -0
  23. tesla_fleet_api/tesla/vehicle/proto/keys_pb2.py +15 -0
  24. tesla_fleet_api/tesla/vehicle/proto/keys_pb2.pyi +21 -0
  25. tesla_fleet_api/tesla/vehicle/proto/managed_charging_pb2.py +15 -0
  26. tesla_fleet_api/tesla/vehicle/proto/managed_charging_pb2.pyi +17 -0
  27. tesla_fleet_api/tesla/vehicle/proto/signatures_pb2.py +35 -0
  28. tesla_fleet_api/tesla/vehicle/proto/signatures_pb2.pyi +152 -0
  29. tesla_fleet_api/tesla/vehicle/proto/universal_message_pb2.py +30 -0
  30. tesla_fleet_api/tesla/vehicle/proto/universal_message_pb2.pyi +148 -0
  31. tesla_fleet_api/tesla/vehicle/proto/vcsec_pb2.py +79 -0
  32. tesla_fleet_api/tesla/vehicle/proto/vcsec_pb2.pyi +482 -0
  33. tesla_fleet_api/tesla/vehicle/proto/vehicle_pb2.py +125 -0
  34. tesla_fleet_api/tesla/vehicle/proto/vehicle_pb2.pyi +1183 -0
  35. tesla_fleet_api/tesla/vehicle/signed.py +55 -0
  36. tesla_fleet_api/tesla/vehicle/vehicle.py +19 -0
  37. tesla_fleet_api/tesla/vehicle/vehicles.py +46 -0
  38. tesla_fleet_api/teslemetry/__init__.py +1 -1
  39. tesla_fleet_api/teslemetry/teslemetry.py +6 -6
  40. tesla_fleet_api/teslemetry/vehicle.py +5 -7
  41. tesla_fleet_api/tessie/__init__.py +1 -1
  42. tesla_fleet_api/tessie/tessie.py +6 -6
  43. tesla_fleet_api/tessie/vehicle.py +3 -9
  44. {tesla_fleet_api-1.0.0.dist-info → tesla_fleet_api-1.0.2.dist-info}/METADATA +1 -1
  45. tesla_fleet_api-1.0.2.dist-info/RECORD +51 -0
  46. tesla_fleet_api-1.0.0.dist-info/RECORD +0 -24
  47. {tesla_fleet_api-1.0.0.dist-info → tesla_fleet_api-1.0.2.dist-info}/LICENSE +0 -0
  48. {tesla_fleet_api-1.0.0.dist-info → tesla_fleet_api-1.0.2.dist-info}/WHEEL +0 -0
  49. {tesla_fleet_api-1.0.0.dist-info → tesla_fleet_api-1.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1284 @@
1
+ from __future__ import annotations
2
+ from abc import abstractmethod
3
+
4
+ import struct
5
+ from random import randbytes
6
+ from typing import Any, TYPE_CHECKING
7
+ import time
8
+ import hmac
9
+ import hashlib
10
+ from cryptography.hazmat.primitives.asymmetric import ec
11
+ from cryptography.hazmat.primitives.serialization import PublicFormat, Encoding
12
+ from cryptography.hazmat.primitives.hashes import Hash, SHA256
13
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
14
+ from asyncio import Lock, sleep
15
+
16
+ from tesla_fleet_api.exceptions import (
17
+ SIGNED_MESSAGE_INFORMATION_FAULTS,
18
+ TeslaFleetMessageFaultIncorrectEpoch,
19
+ TeslaFleetMessageFaultInvalidTokenOrCounter,
20
+ )
21
+
22
+ from tesla_fleet_api.tesla.vehicle.vehicle import Vehicle
23
+
24
+
25
+ from tesla_fleet_api.const import (
26
+ LOGGER,
27
+ Trunk,
28
+ ClimateKeeperMode,
29
+ CabinOverheatProtectionTemp,
30
+ SunRoofCommand,
31
+ WindowCommand,
32
+ )
33
+
34
+ # Protocol
35
+ from tesla_fleet_api.tesla.vehicle.proto.errors_pb2 import GenericError_E
36
+ from tesla_fleet_api.tesla.vehicle.proto.car_server_pb2 import (
37
+ Response,
38
+ )
39
+ from tesla_fleet_api.tesla.vehicle.proto.signatures_pb2 import (
40
+ SIGNATURE_TYPE_AES_GCM_PERSONALIZED,
41
+ SIGNATURE_TYPE_HMAC_PERSONALIZED,
42
+ TAG_COUNTER,
43
+ TAG_DOMAIN,
44
+ TAG_END,
45
+ TAG_EPOCH,
46
+ TAG_EXPIRES_AT,
47
+ TAG_PERSONALIZATION,
48
+ TAG_SIGNATURE_TYPE,
49
+ AES_GCM_Personalized_Signature_Data,
50
+ KeyIdentity,
51
+ SessionInfo,
52
+ SignatureData,
53
+ )
54
+ from tesla_fleet_api.tesla.vehicle.proto.universal_message_pb2 import (
55
+ DOMAIN_INFOTAINMENT,
56
+ DOMAIN_VEHICLE_SECURITY,
57
+ OPERATIONSTATUS_ERROR,
58
+ OPERATIONSTATUS_WAIT,
59
+ Destination,
60
+ Domain,
61
+ RoutableMessage,
62
+ SessionInfoRequest,
63
+ )
64
+ from tesla_fleet_api.tesla.vehicle.proto.vcsec_pb2 import (
65
+ OPERATIONSTATUS_OK,
66
+ FromVCSECMessage,
67
+ )
68
+ from tesla_fleet_api.tesla.vehicle.proto.car_server_pb2 import (
69
+ Action,
70
+ MediaPlayAction,
71
+ VehicleAction,
72
+ VehicleControlFlashLightsAction,
73
+ ChargingStartStopAction,
74
+ ChargingSetLimitAction,
75
+ EraseUserDataAction,
76
+ DrivingClearSpeedLimitPinAction,
77
+ DrivingSetSpeedLimitAction,
78
+ DrivingSpeedLimitAction,
79
+ HvacAutoAction,
80
+ HvacSeatHeaterActions,
81
+ HvacSeatCoolerActions,
82
+ HvacSetPreconditioningMaxAction,
83
+ HvacSteeringWheelHeaterAction,
84
+ HvacTemperatureAdjustmentAction,
85
+ GetNearbyChargingSites,
86
+ VehicleControlCancelSoftwareUpdateAction,
87
+ VehicleControlHonkHornAction,
88
+ VehicleControlResetValetPinAction,
89
+ VehicleControlScheduleSoftwareUpdateAction,
90
+ VehicleControlSetSentryModeAction,
91
+ VehicleControlSetValetModeAction,
92
+ VehicleControlSunroofOpenCloseAction,
93
+ VehicleControlTriggerHomelinkAction,
94
+ VehicleControlWindowAction,
95
+ HvacBioweaponModeAction,
96
+ AutoSeatClimateAction,
97
+ Ping,
98
+ ScheduledChargingAction,
99
+ ScheduledDepartureAction,
100
+ HvacClimateKeeperAction,
101
+ SetChargingAmpsAction,
102
+ SetCabinOverheatProtectionAction,
103
+ SetVehicleNameAction,
104
+ SetCopTempAction,
105
+ VehicleControlSetPinToDriveAction,
106
+ VehicleControlResetPinToDriveAction,
107
+ MediaNextTrack,
108
+ MediaNextFavorite,
109
+ MediaUpdateVolume,
110
+ MediaPreviousTrack,
111
+ MediaPreviousFavorite,
112
+ )
113
+ from tesla_fleet_api.tesla.vehicle.proto.vehicle_pb2 import VehicleState, ClimateState
114
+ from tesla_fleet_api.tesla.vehicle.proto.vcsec_pb2 import (
115
+ UnsignedMessage,
116
+ RKEAction_E,
117
+ ClosureMoveRequest,
118
+ ClosureMoveType_E,
119
+ )
120
+ from tesla_fleet_api.tesla.vehicle.proto.signatures_pb2 import (
121
+ HMAC_Personalized_Signature_Data,
122
+ )
123
+ from tesla_fleet_api.tesla.vehicle.proto.common_pb2 import (
124
+ Void,
125
+ PreconditioningTimes,
126
+ OffPeakChargingTimes,
127
+ )
128
+
129
+ if TYPE_CHECKING:
130
+ from tesla_fleet_api.tesla.tesla import Tesla
131
+
132
+ # ENUMs to convert ints to proto typed ints
133
+ AutoSeatClimatePositions = (
134
+ AutoSeatClimateAction.AutoSeatPosition_FrontLeft,
135
+ AutoSeatClimateAction.AutoSeatPosition_FrontRight,
136
+ )
137
+
138
+ HvacSeatCoolerLevels = (
139
+ HvacSeatCoolerActions.HvacSeatCoolerLevel_Off,
140
+ HvacSeatCoolerActions.HvacSeatCoolerLevel_Low,
141
+ HvacSeatCoolerActions.HvacSeatCoolerLevel_Med,
142
+ HvacSeatCoolerActions.HvacSeatCoolerLevel_High,
143
+ )
144
+
145
+ HvacSeatCoolerPositions = (
146
+ HvacSeatCoolerActions.HvacSeatCoolerPosition_FrontLeft,
147
+ HvacSeatCoolerActions.HvacSeatCoolerPosition_FrontRight,
148
+ )
149
+
150
+ HvacClimateKeeperActions = (
151
+ HvacClimateKeeperAction.ClimateKeeperAction_Off,
152
+ HvacClimateKeeperAction.ClimateKeeperAction_On,
153
+ HvacClimateKeeperAction.ClimateKeeperAction_Dog,
154
+ HvacClimateKeeperAction.ClimateKeeperAction_Camp,
155
+ )
156
+
157
+ CopActivationTemps = (
158
+ ClimateState.CopActivationTemp.CopActivationTempLow,
159
+ ClimateState.CopActivationTemp.CopActivationTempMedium,
160
+ ClimateState.CopActivationTemp.CopActivationTempHigh,
161
+ )
162
+
163
+ class Session:
164
+ """A connect to a domain"""
165
+
166
+ domain: Domain
167
+ parent: Commands
168
+ key: bytes
169
+ counter: int
170
+ epoch: bytes
171
+ delta: int
172
+ sharedKey: bytes
173
+ hmac: bytes
174
+ publicKey: bytes
175
+ lock: Lock
176
+
177
+ def __init__(self, parent: Commands, domain: Domain):
178
+ self.parent = parent
179
+ self.domain = domain
180
+ self.lock = Lock()
181
+ self.counter = 0
182
+ self.ready = False
183
+
184
+ def update(self, sessionInfo: SessionInfo):
185
+ """Update the session with new information"""
186
+
187
+ self.counter = sessionInfo.counter
188
+ self.epoch = sessionInfo.epoch
189
+ self.delta = int(time.time()) - sessionInfo.clock_time
190
+ if (not self.ready or self.publicKey != sessionInfo.publicKey):
191
+ self.publicKey = sessionInfo.publicKey
192
+ self.sharedKey = self.parent.shared_key(sessionInfo.publicKey)
193
+ self.hmac = hmac.new(self.sharedKey, "authenticated command".encode(), hashlib.sha256).digest()
194
+ self.ready = True
195
+
196
+ def hmac_personalized(self) -> HMAC_Personalized_Signature_Data:
197
+ """Sign a command and return session metadata"""
198
+ self.counter += 1
199
+ return HMAC_Personalized_Signature_Data(
200
+ epoch=self.epoch,
201
+ counter=self.counter,
202
+ expires_at=int(time.time()) - self.delta + 10,
203
+ )
204
+
205
+ def aes_gcm_personalized(self) -> AES_GCM_Personalized_Signature_Data:
206
+ """Sign a command and return session metadata"""
207
+ self.counter += 1
208
+ return AES_GCM_Personalized_Signature_Data(
209
+ epoch=self.epoch,
210
+ nonce=randbytes(12),
211
+ counter=self.counter,
212
+ expires_at=int(time.time()) - self.delta + 10,
213
+ )
214
+
215
+
216
+ class Commands(Vehicle):
217
+ """Class describing the Tesla Fleet API vehicle endpoints and commands for a specific vehicle with command signing."""
218
+
219
+ private_key: ec.EllipticCurvePrivateKey
220
+ _public_key: bytes
221
+ _from_destination: bytes
222
+ _sessions: dict[int, Session]
223
+ _require_keys = True
224
+ _auth_method: str
225
+
226
+ def __init__(
227
+ self, parent: Tesla, vin: str, private_key: ec.EllipticCurvePrivateKey | None = None, public_key: bytes | None = None
228
+ ):
229
+ super().__init__(parent, vin)
230
+
231
+ self._from_destination = randbytes(16)
232
+ self._sessions = {
233
+ Domain.DOMAIN_VEHICLE_SECURITY: Session(self, Domain.DOMAIN_VEHICLE_SECURITY),
234
+ Domain.DOMAIN_INFOTAINMENT: Session(self, Domain.DOMAIN_INFOTAINMENT),
235
+ }
236
+
237
+ if(self._require_keys):
238
+ if private_key:
239
+ self.private_key = private_key
240
+ elif parent.private_key:
241
+ self.private_key = parent.private_key
242
+ else:
243
+ raise ValueError("No private key.")
244
+
245
+ self._public_key = public_key or self.private_key.public_key().public_bytes(
246
+ encoding=Encoding.X962, format=PublicFormat.UncompressedPoint
247
+ )
248
+
249
+
250
+ def shared_key(self, vehicleKey: bytes) -> bytes:
251
+ exchange = self.private_key.exchange(
252
+ ec.ECDH(),
253
+ ec.EllipticCurvePublicKey.from_encoded_point(
254
+ ec.SECP256R1(), vehicleKey
255
+ ),
256
+ )
257
+ return hashlib.sha1(exchange).digest()[:16]
258
+
259
+
260
+ @abstractmethod
261
+ async def _send(self, msg: RoutableMessage) -> RoutableMessage:
262
+ """Transmit the message to the vehicle."""
263
+ raise NotImplementedError
264
+
265
+ @abstractmethod
266
+ async def _command(self, domain: Domain, command: bytes, attempt: int = 0) -> dict[str, Any]:
267
+ """Serialize a message and send to the signed command endpoint."""
268
+ session = self._sessions[domain]
269
+ if not session.ready:
270
+ await self._handshake(domain)
271
+
272
+ if self._auth_method == "hmac":
273
+ msg = await self._commandHmac(session, command)
274
+ elif self._auth_method == "aes":
275
+ msg = await self._commandAes(session, command)
276
+ else:
277
+ raise ValueError(f"Unknown auth method: {self._auth_method}")
278
+
279
+ try:
280
+ resp = await self._send(msg)
281
+ except (
282
+ TeslaFleetMessageFaultIncorrectEpoch,
283
+ TeslaFleetMessageFaultInvalidTokenOrCounter,
284
+ ) as e:
285
+ attempt += 1
286
+ if attempt > 3:
287
+ # We tried 3 times, give up, raise the error
288
+ raise e
289
+ return await self._command(domain, command, attempt)
290
+
291
+ if resp.signedMessageStatus.operation_status == OPERATIONSTATUS_WAIT:
292
+ attempt += 1
293
+ if attempt > 3:
294
+ # We tried 3 times, give up, raise the error
295
+ return {"response": {"result": False, "reason": "Too many retries"}}
296
+ async with session.lock:
297
+ await sleep(2)
298
+ return await self._command(domain, command, attempt)
299
+
300
+ if resp.HasField("protobuf_message_as_bytes"):
301
+ if(resp.from_destination.domain == DOMAIN_VEHICLE_SECURITY):
302
+ vcsec = FromVCSECMessage.FromString(resp.protobuf_message_as_bytes)
303
+ LOGGER.debug("VCSEC Response: %s", vcsec)
304
+ if vcsec.HasField("nominalError"):
305
+ LOGGER.error("Command failed with reason: %s", vcsec.nominalError.genericError)
306
+ return {
307
+ "response": {
308
+ "result": False,
309
+ "reason": GenericError_E.Name(vcsec.nominalError.genericError)
310
+ }
311
+ }
312
+ elif vcsec.commandStatus.operationStatus == OPERATIONSTATUS_OK:
313
+ return {"response": {"result": True, "reason": ""}}
314
+ elif vcsec.commandStatus.operationStatus == OPERATIONSTATUS_WAIT:
315
+ attempt += 1
316
+ if attempt > 3:
317
+ # We tried 3 times, give up, raise the error
318
+ return {"response": {"result": False, "reason": "Too many retries"}}
319
+ async with session.lock:
320
+ await sleep(2)
321
+ return await self._command(domain, command, attempt)
322
+ elif vcsec.commandStatus.operationStatus == OPERATIONSTATUS_ERROR:
323
+ if(resp.HasField("signedMessageStatus")):
324
+ raise SIGNED_MESSAGE_INFORMATION_FAULTS[vcsec.commandStatus.signedMessageStatus.signedMessageInformation]
325
+
326
+ elif(resp.from_destination.domain == DOMAIN_INFOTAINMENT):
327
+ response = Response.FromString(resp.protobuf_message_as_bytes)
328
+ LOGGER.debug("Infotainment Response: %s", response)
329
+ if (response.HasField("ping")):
330
+ print(response.ping)
331
+ return {
332
+ "response": {
333
+ "result": True,
334
+ "reason": response.ping.local_timestamp
335
+ }
336
+ }
337
+ if response.HasField("actionStatus"):
338
+ return {
339
+ "response": {
340
+ "result": response.actionStatus.result == OPERATIONSTATUS_OK,
341
+ "reason": response.actionStatus.result_reason.plain_text or ""
342
+ }
343
+ }
344
+
345
+ return {"response": {"result": True, "reason": ""}}
346
+
347
+ async def _commandHmac(self, session: Session, command: bytes, attempt: int = 1) -> RoutableMessage:
348
+ """Create a signed message."""
349
+ LOGGER.debug(f"Sending HMAC to domain {Domain.Name(session.domain)}")
350
+
351
+ hmac_personalized = session.hmac_personalized()
352
+
353
+ metadata = bytes([
354
+ TAG_SIGNATURE_TYPE,
355
+ 1,
356
+ SIGNATURE_TYPE_HMAC_PERSONALIZED,
357
+ TAG_DOMAIN,
358
+ 1,
359
+ session.domain,
360
+ TAG_PERSONALIZATION,
361
+ 17,
362
+ *self.vin.encode(),
363
+ TAG_EPOCH,
364
+ len(hmac_personalized.epoch),
365
+ *hmac_personalized.epoch,
366
+ TAG_EXPIRES_AT,
367
+ 4,
368
+ *struct.pack(">I", hmac_personalized.expires_at),
369
+ TAG_COUNTER,
370
+ 4,
371
+ *struct.pack(">I", hmac_personalized.counter),
372
+ TAG_END,
373
+ ])
374
+
375
+ hmac_personalized.tag = hmac.new(
376
+ session.hmac, metadata + command, hashlib.sha256
377
+ ).digest()
378
+
379
+ return RoutableMessage(
380
+ to_destination=Destination(
381
+ domain=session.domain,
382
+ ),
383
+ from_destination=Destination(
384
+ routing_address=self._from_destination
385
+ ),
386
+ protobuf_message_as_bytes=command,
387
+ uuid=randbytes(16),
388
+ signature_data=SignatureData(
389
+ signer_identity=KeyIdentity(
390
+ public_key=self._public_key
391
+ ),
392
+ HMAC_Personalized_data=hmac_personalized,
393
+ )
394
+ )
395
+
396
+ async def _commandAes(self, session: Session, command: bytes, attempt: int = 1) -> RoutableMessage:
397
+ """Create an encrypted message."""
398
+ LOGGER.debug(f"Sending AES to domain {Domain.Name(session.domain)}")
399
+
400
+ aes_personalized = session.aes_gcm_personalized()
401
+
402
+ metadata = bytes([
403
+ TAG_SIGNATURE_TYPE,
404
+ 1,
405
+ SIGNATURE_TYPE_AES_GCM_PERSONALIZED,
406
+ TAG_DOMAIN,
407
+ 1,
408
+ session.domain,
409
+ TAG_PERSONALIZATION,
410
+ 17,
411
+ *self.vin.encode(),
412
+ TAG_EPOCH,
413
+ len(aes_personalized.epoch),
414
+ *aes_personalized.epoch,
415
+ TAG_EXPIRES_AT,
416
+ 4,
417
+ *struct.pack(">I", aes_personalized.expires_at),
418
+ TAG_COUNTER,
419
+ 4,
420
+ *struct.pack(">I", aes_personalized.counter),
421
+ TAG_END,
422
+ ])
423
+
424
+ aad = Hash(SHA256())
425
+ aad.update(metadata)
426
+
427
+ aesgcm = AESGCM(session.sharedKey)
428
+ ct = aesgcm.encrypt(aes_personalized.nonce, command, aad.finalize())
429
+
430
+ aes_personalized.tag = ct[-16:]
431
+
432
+ # I think this whole section could be improved
433
+ return RoutableMessage(
434
+ to_destination=Destination(
435
+ domain=session.domain,
436
+ ),
437
+ from_destination=Destination(
438
+ routing_address=self._from_destination
439
+ ),
440
+ protobuf_message_as_bytes=ct[:-16],
441
+ uuid=randbytes(16),
442
+ signature_data=SignatureData(
443
+ signer_identity=KeyIdentity(
444
+ public_key=self._public_key
445
+ ),
446
+ AES_GCM_Personalized_data=aes_personalized,
447
+ )
448
+ )
449
+
450
+
451
+ async def _sendVehicleSecurity(self, command: UnsignedMessage) -> dict[str, Any]:
452
+ """Sign and send a message to Infotainment computer."""
453
+ return await self._command(Domain.DOMAIN_VEHICLE_SECURITY, command.SerializeToString())
454
+
455
+ async def _sendInfotainment(self, command: Action) -> dict[str, Any]:
456
+ """Sign and send a message to Infotainment computer."""
457
+ return await self._command(Domain.DOMAIN_INFOTAINMENT, command.SerializeToString())
458
+
459
+ async def handshakeVehicleSecurity(self) -> None:
460
+ """Perform a handshake with the vehicle security domain."""
461
+ await self._handshake(Domain.DOMAIN_VEHICLE_SECURITY)
462
+
463
+ async def handshakeInfotainment(self) -> None:
464
+ """Perform a handshake with the infotainment domain."""
465
+ await self._handshake(Domain.DOMAIN_INFOTAINMENT)
466
+
467
+ async def _handshake(self, domain: Domain) -> None:
468
+ """Perform a handshake with the vehicle."""
469
+
470
+ LOGGER.debug(f"Handshake with domain {Domain.Name(domain)}")
471
+ msg = RoutableMessage(
472
+ to_destination=Destination(
473
+ domain=domain,
474
+ ),
475
+ from_destination=Destination(
476
+ routing_address=self._from_destination
477
+ ),
478
+ session_info_request=SessionInfoRequest(
479
+ public_key=self._public_key
480
+ ),
481
+ uuid=randbytes(16)
482
+ )
483
+
484
+ await self._send(msg)
485
+
486
+ async def ping(self) -> dict[str, Any]:
487
+ """Ping the vehicle."""
488
+ return await self._sendInfotainment(
489
+ Action(
490
+ vehicleAction=VehicleAction(
491
+ ping=Ping(ping_id=0)
492
+ )
493
+ )
494
+ )
495
+
496
+ async def actuate_trunk(self, which_trunk: Trunk | str) -> dict[str, Any]:
497
+ """Controls the front or rear trunk."""
498
+ if which_trunk == Trunk.FRONT:
499
+ return await self._sendVehicleSecurity(
500
+ UnsignedMessage(
501
+ closureMoveRequest=ClosureMoveRequest(
502
+ frontTrunk=ClosureMoveType_E.CLOSURE_MOVE_TYPE_MOVE
503
+ )
504
+ )
505
+ )
506
+ if which_trunk == Trunk.REAR:
507
+ return await self._sendVehicleSecurity(
508
+ UnsignedMessage(
509
+ closureMoveRequest=ClosureMoveRequest(
510
+ rearTrunk=ClosureMoveType_E.CLOSURE_MOVE_TYPE_MOVE
511
+ )
512
+ )
513
+ )
514
+ raise ValueError("Invalid trunk.")
515
+
516
+ async def adjust_volume(self, volume: float) -> dict[str, Any]:
517
+ """Adjusts vehicle media playback volume."""
518
+ return await self._sendInfotainment(
519
+ Action(
520
+ vehicleAction=VehicleAction(
521
+ mediaUpdateVolume=MediaUpdateVolume(volume_absolute_float=volume)
522
+ )
523
+ )
524
+ )
525
+
526
+ async def auto_conditioning_start(self) -> dict[str, Any]:
527
+ """Starts climate preconditioning."""
528
+ return await self._sendInfotainment(
529
+ Action(
530
+ vehicleAction=VehicleAction(
531
+ hvacAutoAction=HvacAutoAction(power_on=True)
532
+ )
533
+ )
534
+ )
535
+
536
+ async def auto_conditioning_stop(self) -> dict[str, Any]:
537
+ """Stops climate preconditioning."""
538
+ return await self._sendInfotainment(
539
+ Action(
540
+ vehicleAction=VehicleAction(
541
+ hvacAutoAction=HvacAutoAction(power_on=False)
542
+ )
543
+ )
544
+ )
545
+
546
+ async def cancel_software_update(self) -> dict[str, Any]:
547
+ """Cancels the countdown to install the vehicle software update."""
548
+ return await self._sendInfotainment(
549
+ Action(
550
+ vehicleAction=VehicleAction(
551
+ vehicleControlCancelSoftwareUpdateAction=VehicleControlCancelSoftwareUpdateAction()
552
+ )
553
+ )
554
+ )
555
+
556
+ async def charge_max_range(self) -> dict[str, Any]:
557
+ """Charges in max range mode -- we recommend limiting the use of this mode to long trips."""
558
+ return await self._sendInfotainment(
559
+ Action(
560
+ vehicleAction=VehicleAction(
561
+ chargingStartStopAction=ChargingStartStopAction(
562
+ start_max_range=Void()
563
+ )
564
+ )
565
+ )
566
+ )
567
+
568
+ async def charge_port_door_close(self) -> dict[str, Any]:
569
+ """Closes the charge port door."""
570
+ return await self._sendVehicleSecurity(
571
+ UnsignedMessage(
572
+ closureMoveRequest=ClosureMoveRequest(
573
+ chargePort=ClosureMoveType_E.CLOSURE_MOVE_TYPE_CLOSE
574
+ )
575
+ )
576
+ )
577
+
578
+ async def charge_port_door_open(self) -> dict[str, Any]:
579
+ """Opens the charge port door."""
580
+ return await self._sendVehicleSecurity(
581
+ UnsignedMessage(
582
+ closureMoveRequest=ClosureMoveRequest(
583
+ chargePort=ClosureMoveType_E.CLOSURE_MOVE_TYPE_OPEN
584
+ )
585
+ )
586
+ )
587
+
588
+ async def charge_standard(self) -> dict[str, Any]:
589
+ """Charges in Standard mode."""
590
+ return await self._sendInfotainment(
591
+ Action(
592
+ vehicleAction=VehicleAction(
593
+ chargingStartStopAction=ChargingStartStopAction(
594
+ start_standard=Void()
595
+ )
596
+ )
597
+ )
598
+ )
599
+
600
+ async def charge_start(self) -> dict[str, Any]:
601
+ """Starts charging the vehicle."""
602
+ return await self._sendInfotainment(
603
+ Action(
604
+ vehicleAction=VehicleAction(
605
+ chargingStartStopAction=ChargingStartStopAction(start=Void())
606
+ )
607
+ )
608
+ )
609
+
610
+ async def charge_stop(self) -> dict[str, Any]:
611
+ """Stops charging the vehicle."""
612
+ return await self._sendInfotainment(
613
+ Action(
614
+ vehicleAction=VehicleAction(
615
+ chargingStartStopAction=ChargingStartStopAction(stop=Void())
616
+ )
617
+ )
618
+ )
619
+
620
+ async def clear_pin_to_drive_admin(self, pin: str | None = None):
621
+ """Deactivates PIN to Drive and resets the associated PIN for vehicles running firmware versions 2023.44+. This command is only accessible to fleet managers or owners."""
622
+ return await self._sendInfotainment(
623
+ Action(
624
+ vehicleAction=VehicleAction(
625
+ drivingClearSpeedLimitPinAction=DrivingClearSpeedLimitPinAction(
626
+ pin=pin
627
+ )
628
+ )
629
+ )
630
+ )
631
+
632
+ async def door_lock(self) -> dict[str, Any]:
633
+ """Locks the vehicle."""
634
+ return await self._sendVehicleSecurity(
635
+ UnsignedMessage(RKEAction=RKEAction_E.RKE_ACTION_LOCK)
636
+ )
637
+
638
+ async def door_unlock(self) -> dict[str, Any]:
639
+ """Unlocks the vehicle."""
640
+ return await self._sendVehicleSecurity(
641
+ UnsignedMessage(RKEAction=RKEAction_E.RKE_ACTION_UNLOCK)
642
+ )
643
+
644
+ async def erase_user_data(self) -> dict[str, Any]:
645
+ """Erases user's data from the user interface. Requires the vehicle to be in park."""
646
+ return await self._sendInfotainment(
647
+ Action(
648
+ vehicleAction=VehicleAction(eraseUserDataAction=EraseUserDataAction())
649
+ )
650
+ )
651
+
652
+ async def flash_lights(self) -> dict[str, Any]:
653
+ """Briefly flashes the vehicle headlights. Requires the vehicle to be in park."""
654
+ return await self._sendInfotainment(
655
+ Action(
656
+ vehicleAction=VehicleAction(
657
+ vehicleControlFlashLightsAction=VehicleControlFlashLightsAction()
658
+ )
659
+ )
660
+ )
661
+
662
+ async def guest_mode(self, enable: bool) -> dict[str, Any]:
663
+ """Restricts certain vehicle UI functionality from guest users"""
664
+ return await self._sendInfotainment(
665
+ Action(
666
+ vehicleAction=VehicleAction(
667
+ guestModeAction=VehicleState.GuestMode(GuestModeActive=enable)
668
+ )
669
+ )
670
+ )
671
+
672
+ async def honk_horn(self) -> dict[str, Any]:
673
+ """Honks the vehicle horn. Requires the vehicle to be in park."""
674
+ return await self._sendInfotainment(
675
+ Action(
676
+ vehicleAction=VehicleAction(
677
+ vehicleControlHonkHornAction=VehicleControlHonkHornAction()
678
+ )
679
+ )
680
+ )
681
+
682
+ async def media_next_fav(self) -> dict[str, Any]:
683
+ """Advances media player to next favorite track."""
684
+ return await self._sendInfotainment(
685
+ Action(vehicleAction=VehicleAction(mediaNextFavorite=MediaNextFavorite()))
686
+ )
687
+
688
+ async def media_next_track(self) -> dict[str, Any]:
689
+ """Advances media player to next track."""
690
+ return await self._sendInfotainment(
691
+ Action(vehicleAction=VehicleAction(mediaNextTrack=MediaNextTrack()))
692
+ )
693
+
694
+ async def media_prev_fav(self) -> dict[str, Any]:
695
+ """Advances media player to previous favorite track."""
696
+ return await self._sendInfotainment(
697
+ Action(
698
+ vehicleAction=VehicleAction(
699
+ mediaPreviousFavorite=MediaPreviousFavorite()
700
+ )
701
+ )
702
+ )
703
+
704
+ async def media_prev_track(self) -> dict[str, Any]:
705
+ """Advances media player to previous track."""
706
+ return await self._sendInfotainment(
707
+ Action(vehicleAction=VehicleAction(mediaPreviousTrack=MediaPreviousTrack()))
708
+ )
709
+
710
+ async def media_toggle_playback(self) -> dict[str, Any]:
711
+ """Toggles current play/pause state."""
712
+ return await self._sendInfotainment(
713
+ Action(vehicleAction=VehicleAction(mediaPlayAction=MediaPlayAction()))
714
+ )
715
+
716
+ async def media_volume_down(self) -> dict[str, Any]:
717
+ """Turns the volume down by one."""
718
+ return await self._sendInfotainment(
719
+ Action(
720
+ vehicleAction=VehicleAction(
721
+ mediaUpdateVolume=MediaUpdateVolume(volume_delta=-1)
722
+ )
723
+ )
724
+ )
725
+
726
+ # This one is new
727
+ async def media_volume_up(self) -> dict[str, Any]:
728
+ """Turns the volume up by one."""
729
+ return await self._sendInfotainment(
730
+ Action(
731
+ vehicleAction=VehicleAction(
732
+ mediaUpdateVolume=MediaUpdateVolume(volume_delta=1)
733
+ )
734
+ )
735
+ )
736
+
737
+ # navigation_gps_request doesnt require signing
738
+ # navigation_request doesnt require signing
739
+ # navigation_sc_request doesnt require signing
740
+ #
741
+
742
+ async def remote_auto_seat_climate_request(
743
+ self, auto_seat_position: int, auto_climate_on: bool
744
+ ) -> dict[str, Any]:
745
+ """Sets automatic seat heating and cooling."""
746
+ # AutoSeatPosition_FrontLeft = 1;
747
+ # AutoSeatPosition_FrontRight = 2;
748
+ return await self._sendInfotainment(
749
+ Action(
750
+ vehicleAction=VehicleAction(
751
+ autoSeatClimateAction=AutoSeatClimateAction(
752
+ carseat=[
753
+ AutoSeatClimateAction.CarSeat(
754
+ on=auto_climate_on, seat_position=AutoSeatClimatePositions[auto_seat_position]
755
+ )
756
+ ]
757
+ )
758
+ )
759
+ )
760
+ )
761
+
762
+ # remote_auto_steering_wheel_heat_climate_request has no protobuf
763
+
764
+ # remote_boombox not implemented
765
+ #
766
+
767
+
768
+ async def remote_seat_cooler_request(
769
+ self, seat_position: int, seat_cooler_level: int
770
+ ) -> dict[str, Any]:
771
+ """Sets seat cooling."""
772
+ # HvacSeatCoolerLevel_Unknown = 0;
773
+ # HvacSeatCoolerLevel_Off = 1;
774
+ # HvacSeatCoolerLevel_Low = 2;
775
+ # HvacSeatCoolerLevel_Med = 3;
776
+ # HvacSeatCoolerLevel_High = 4;
777
+ # HvacSeatCoolerPosition_Unknown = 0;
778
+ # HvacSeatCoolerPosition_FrontLeft = 1;
779
+ # HvacSeatCoolerPosition_FrontRight = 2;
780
+ return await self._sendInfotainment(
781
+ Action(
782
+ vehicleAction=VehicleAction(
783
+ hvacSeatCoolerActions=HvacSeatCoolerActions(
784
+ hvacSeatCoolerAction=[
785
+ HvacSeatCoolerActions.HvacSeatCoolerAction(
786
+ seat_cooler_level=HvacSeatCoolerLevels[seat_cooler_level],
787
+ seat_position=HvacSeatCoolerPositions[seat_position],
788
+ )
789
+ ]
790
+ )
791
+ )
792
+ )
793
+ )
794
+
795
+ async def remote_seat_heater_request(
796
+ self, seat_position: int, seat_heater_level: int
797
+ ) -> dict[str, Any]:
798
+ """Sets seat heating."""
799
+ # HvacSeatCoolerLevel_Unknown = 0;
800
+ # HvacSeatCoolerLevel_Off = 1;
801
+ # HvacSeatCoolerLevel_Low = 2;
802
+ # HvacSeatCoolerLevel_Med = 3;
803
+ # HvacSeatCoolerLevel_High = 4;
804
+ # Void CAR_SEAT_UNKNOWN = 6;
805
+ # Void CAR_SEAT_FRONT_LEFT = 7;
806
+ # Void CAR_SEAT_FRONT_RIGHT = 8;
807
+ # Void CAR_SEAT_REAR_LEFT = 9;
808
+ # Void CAR_SEAT_REAR_LEFT_BACK = 10;
809
+ # Void CAR_SEAT_REAR_CENTER = 11;
810
+ # Void CAR_SEAT_REAR_RIGHT = 12;
811
+ # Void CAR_SEAT_REAR_RIGHT_BACK = 13;
812
+ # Void CAR_SEAT_THIRD_ROW_LEFT = 14;
813
+ # Void CAR_SEAT_THIRD_ROW_RIGHT = 15;
814
+
815
+ heater_action_dict = {}
816
+ match seat_position:
817
+ case 0:
818
+ heater_action_dict["CAR_SEAT_FRONT_LEFT"] = Void()
819
+ case 1:
820
+ heater_action_dict["CAR_SEAT_FRONT_RIGHT"] = Void()
821
+ case 2:
822
+ heater_action_dict["CAR_SEAT_REAR_LEFT"] = Void()
823
+ case 3:
824
+ heater_action_dict["CAR_SEAT_REAR_LEFT_BACK"] = Void()
825
+ case 4:
826
+ heater_action_dict["CAR_SEAT_REAR_CENTER"] = Void()
827
+ case 5:
828
+ heater_action_dict["CAR_SEAT_REAR_RIGHT"] = Void()
829
+ case 6:
830
+ heater_action_dict["CAR_SEAT_REAR_RIGHT_BACK"] = Void()
831
+ case 7:
832
+ heater_action_dict["CAR_SEAT_THIRD_ROW_LEFT"] = Void()
833
+ case 8:
834
+ heater_action_dict["CAR_SEAT_THIRD_ROW_RIGHT"] = Void()
835
+ case _:
836
+ raise ValueError(f"Invalid seat position: {seat_position}")
837
+ match seat_heater_level:
838
+ case 0:
839
+ heater_action_dict["SEAT_HEATER_OFF"] = Void()
840
+ case 1:
841
+ heater_action_dict["SEAT_HEATER_LOW"] = Void()
842
+ case 2:
843
+ heater_action_dict["SEAT_HEATER_MEDIUM"] = Void()
844
+ case 3:
845
+ heater_action_dict["SEAT_HEATER_HIGH"] = Void()
846
+ case _:
847
+ raise ValueError(f"Invalid seat heater level: {seat_heater_level}")
848
+
849
+ heater_action = HvacSeatHeaterActions.HvacSeatHeaterAction(**heater_action_dict)
850
+ return await self._sendInfotainment(
851
+ Action(
852
+ vehicleAction=VehicleAction(
853
+ hvacSeatHeaterActions=HvacSeatHeaterActions(
854
+ hvacSeatHeaterAction=[heater_action]
855
+ )
856
+ )
857
+ )
858
+ )
859
+
860
+ async def remote_start_drive(self) -> dict[str, Any]:
861
+ """Starts the vehicle remotely. Requires keyless driving to be enabled."""
862
+ return await self._sendVehicleSecurity(
863
+ UnsignedMessage(RKEAction=RKEAction_E.RKE_ACTION_REMOTE_DRIVE)
864
+ )
865
+
866
+ async def remote_steering_wheel_heat_level_request(
867
+ self, level: int
868
+ ) -> dict[str, Any]:
869
+ """Sets steering wheel heat level."""
870
+ raise NotImplementedError()
871
+
872
+ async def remote_steering_wheel_heater_request(self, on: bool) -> dict[str, Any]:
873
+ """Sets steering wheel heating on/off. For vehicles that do not support auto steering wheel heat."""
874
+ return await self._sendInfotainment(
875
+ Action(
876
+ vehicleAction=VehicleAction(
877
+ hvacSteeringWheelHeaterAction=HvacSteeringWheelHeaterAction(
878
+ power_on=on
879
+ )
880
+ )
881
+ )
882
+ )
883
+
884
+ async def reset_pin_to_drive_pin(self) -> dict[str, Any]:
885
+ """Removes PIN to Drive. Requires the car to be in Pin to Drive mode and not in Valet mode. Note that this only works if PIN to Drive is not active. This command also requires the Tesla Vehicle Command Protocol - for more information, please see refer to the documentation here."""
886
+ return await self._sendInfotainment(
887
+ Action(
888
+ vehicleAction=VehicleAction(
889
+ vehicleControlResetPinToDriveAction=VehicleControlResetPinToDriveAction()
890
+ )
891
+ )
892
+ )
893
+
894
+ async def reset_valet_pin(self) -> dict[str, Any]:
895
+ """Removes PIN for Valet Mode."""
896
+ return await self._sendInfotainment(
897
+ Action(
898
+ vehicleAction=VehicleAction(
899
+ vehicleControlResetValetPinAction=VehicleControlResetValetPinAction()
900
+ )
901
+ )
902
+ )
903
+
904
+ async def schedule_software_update(self, offset_sec: int) -> dict[str, Any]:
905
+ """Schedules a vehicle software update (over the air "OTA") to be installed in the future."""
906
+ return await self._sendInfotainment(
907
+ Action(
908
+ vehicleAction=VehicleAction(
909
+ vehicleControlScheduleSoftwareUpdateAction=VehicleControlScheduleSoftwareUpdateAction(
910
+ offset_sec=offset_sec
911
+ )
912
+ )
913
+ )
914
+ )
915
+
916
+ async def set_bioweapon_mode(
917
+ self, on: bool, manual_override: bool
918
+ ) -> dict[str, Any]:
919
+ """Turns Bioweapon Defense Mode on and off."""
920
+ return await self._sendInfotainment(
921
+ Action(
922
+ vehicleAction=VehicleAction(
923
+ hvacBioweaponModeAction=HvacBioweaponModeAction(
924
+ on=on, manual_override=manual_override
925
+ )
926
+ )
927
+ )
928
+ )
929
+
930
+ async def set_cabin_overheat_protection(
931
+ self, on: bool, fan_only: bool
932
+ ) -> dict[str, Any]:
933
+ """Sets the vehicle overheat protection."""
934
+ return await self._sendInfotainment(
935
+ Action(
936
+ vehicleAction=VehicleAction(
937
+ setCabinOverheatProtectionAction=SetCabinOverheatProtectionAction(
938
+ on=on, fan_only=fan_only
939
+ )
940
+ )
941
+ )
942
+ )
943
+
944
+ async def set_charge_limit(self, percent: int) -> dict[str, Any]:
945
+ """Sets the vehicle charge limit."""
946
+ return await self._sendInfotainment(
947
+ Action(
948
+ vehicleAction=VehicleAction(
949
+ chargingSetLimitAction=ChargingSetLimitAction(percent=percent)
950
+ )
951
+ )
952
+ )
953
+
954
+ async def set_charging_amps(self, charging_amps: int) -> dict[str, Any]:
955
+ """Sets the vehicle charging amps."""
956
+ return await self._sendInfotainment(
957
+ Action(
958
+ vehicleAction=VehicleAction(
959
+ setChargingAmpsAction=SetChargingAmpsAction(
960
+ charging_amps=charging_amps
961
+ )
962
+ )
963
+ )
964
+ )
965
+
966
+ async def set_climate_keeper_mode(
967
+ self, climate_keeper_mode: ClimateKeeperMode | int
968
+ ) -> dict[str, Any]:
969
+ """Enables climate keeper mode."""
970
+ if isinstance(climate_keeper_mode, ClimateKeeperMode):
971
+ climate_keeper_mode = climate_keeper_mode.value
972
+
973
+ return await self._sendInfotainment(
974
+ Action(
975
+ vehicleAction=VehicleAction(
976
+ hvacClimateKeeperAction=HvacClimateKeeperAction(
977
+ ClimateKeeperAction=HvacClimateKeeperActions[climate_keeper_mode],
978
+ # manual_override
979
+ )
980
+ )
981
+ )
982
+ )
983
+
984
+ async def set_cop_temp(
985
+ self, cop_temp: CabinOverheatProtectionTemp | int
986
+ ) -> dict[str, Any]:
987
+ """Adjusts the Cabin Overheat Protection temperature (COP)."""
988
+ if isinstance(cop_temp, CabinOverheatProtectionTemp):
989
+ cop_temp = cop_temp.value
990
+ return await self._sendInfotainment(
991
+ Action(
992
+ vehicleAction=VehicleAction(
993
+ setCopTempAction=SetCopTempAction(copActivationTemp=CopActivationTemps[cop_temp])
994
+ )
995
+ )
996
+ )
997
+
998
+ async def set_pin_to_drive(self, on: bool, password: str | int) -> dict[str, Any]:
999
+ """Sets a four-digit passcode for PIN to Drive. This PIN must then be entered before the vehicle can be driven."""
1000
+ return await self._sendInfotainment(
1001
+ Action(
1002
+ vehicleAction=VehicleAction(
1003
+ vehicleControlSetPinToDriveAction=VehicleControlSetPinToDriveAction(
1004
+ on=on, password=str(password)
1005
+ )
1006
+ )
1007
+ )
1008
+ )
1009
+
1010
+ async def set_preconditioning_max(
1011
+ self, on: bool, manual_override: bool
1012
+ ) -> dict[str, Any]:
1013
+ """Sets an override for preconditioning — it should default to empty if no override is used."""
1014
+ return await self._sendInfotainment(
1015
+ Action(
1016
+ vehicleAction=VehicleAction(
1017
+ hvacSetPreconditioningMaxAction=HvacSetPreconditioningMaxAction(
1018
+ on=on,
1019
+ manual_override=manual_override,
1020
+ # manual_override_mode
1021
+ )
1022
+ )
1023
+ )
1024
+ )
1025
+
1026
+ async def set_scheduled_charging(self, enable: bool, time: int) -> dict[str, Any]:
1027
+ """Sets a time at which charging should be completed. The time parameter is minutes after midnight (e.g: time=120 schedules charging for 2:00am vehicle local time)."""
1028
+ return await self._sendInfotainment(
1029
+ Action(
1030
+ vehicleAction=VehicleAction(
1031
+ scheduledChargingAction=ScheduledChargingAction(
1032
+ enabled=enable, charging_time=time
1033
+ )
1034
+ )
1035
+ )
1036
+ )
1037
+
1038
+ async def set_scheduled_departure(
1039
+ self,
1040
+ enable: bool = True,
1041
+ preconditioning_enabled: bool = False,
1042
+ preconditioning_weekdays_only: bool = False,
1043
+ departure_time: int = 0,
1044
+ off_peak_charging_enabled: bool = False,
1045
+ off_peak_charging_weekdays_only: bool = False,
1046
+ end_off_peak_time: int = 0,
1047
+ ) -> dict[str, Any]:
1048
+ """Sets a time at which departure should be completed. The time parameter is minutes after midnight (e.g: time=120 schedules departure for 2:00am vehicle local time)."""
1049
+
1050
+ if preconditioning_weekdays_only:
1051
+ preconditioning_times = PreconditioningTimes(weekdays=Void())
1052
+ else:
1053
+ preconditioning_times = PreconditioningTimes(all_week=Void())
1054
+
1055
+ if off_peak_charging_weekdays_only:
1056
+ off_peak_charging_times = OffPeakChargingTimes(weekdays=Void())
1057
+ else:
1058
+ off_peak_charging_times = OffPeakChargingTimes(all_week=Void())
1059
+
1060
+ return await self._sendInfotainment(
1061
+ Action(
1062
+ vehicleAction=VehicleAction(
1063
+ scheduledDepartureAction=ScheduledDepartureAction(
1064
+ enabled=enable,
1065
+ departure_time=departure_time,
1066
+ preconditioning_times=preconditioning_times,
1067
+ off_peak_charging_times=off_peak_charging_times,
1068
+ off_peak_hours_end_time=end_off_peak_time,
1069
+ )
1070
+ )
1071
+ )
1072
+ )
1073
+
1074
+ async def set_sentry_mode(self, on: bool) -> dict[str, Any]:
1075
+ """Enables and disables Sentry Mode. Sentry Mode allows customers to watch the vehicle cameras live from the mobile app, as well as record sentry events."""
1076
+ return await self._sendInfotainment(
1077
+ Action(
1078
+ vehicleAction=VehicleAction(
1079
+ vehicleControlSetSentryModeAction=VehicleControlSetSentryModeAction(
1080
+ on=on
1081
+ )
1082
+ )
1083
+ )
1084
+ )
1085
+
1086
+ async def set_temps(
1087
+ self, driver_temp: float, passenger_temp: float
1088
+ ) -> dict[str, Any]:
1089
+ """Sets the driver and/or passenger-side cabin temperature (and other zones if sync is enabled)."""
1090
+ return await self._sendInfotainment(
1091
+ Action(
1092
+ vehicleAction=VehicleAction(
1093
+ hvacTemperatureAdjustmentAction=HvacTemperatureAdjustmentAction(
1094
+ driver_temp_celsius=driver_temp,
1095
+ passenger_temp_celsius=passenger_temp,
1096
+ )
1097
+ )
1098
+ )
1099
+ )
1100
+
1101
+ async def set_valet_mode(self, on: bool, password: str | int) -> dict[str, Any]:
1102
+ """Turns on Valet Mode and sets a four-digit passcode that must then be entered to disable Valet Mode."""
1103
+ return await self._sendInfotainment(
1104
+ Action(
1105
+ vehicleAction=VehicleAction(
1106
+ vehicleControlSetValetModeAction=VehicleControlSetValetModeAction(
1107
+ on=on, password=str(password)
1108
+ )
1109
+ )
1110
+ )
1111
+ )
1112
+
1113
+ async def set_vehicle_name(self, vehicle_name: str) -> dict[str, Any]:
1114
+ """Changes the name of a vehicle. This command also requires the Tesla Vehicle Command Protocol - for more information, please see refer to the documentation here."""
1115
+ return await self._sendInfotainment(
1116
+ Action(
1117
+ vehicleAction=VehicleAction(
1118
+ setVehicleNameAction=SetVehicleNameAction(vehicleName=vehicle_name)
1119
+ )
1120
+ )
1121
+ )
1122
+
1123
+ async def speed_limit_activate(self, pin: str | int) -> dict[str, Any]:
1124
+ """Activates Speed Limit Mode with a four-digit PIN."""
1125
+ return await self._sendInfotainment(
1126
+ Action(
1127
+ vehicleAction=VehicleAction(
1128
+ drivingSpeedLimitAction=DrivingSpeedLimitAction(
1129
+ activate=True, pin=str(pin)
1130
+ )
1131
+ )
1132
+ )
1133
+ )
1134
+
1135
+ async def speed_limit_clear_pin(self, pin: str | int) -> dict[str, Any]:
1136
+ """Deactivates Speed Limit Mode and resets the associated PIN."""
1137
+ return await self._sendInfotainment(
1138
+ Action(
1139
+ vehicleAction=VehicleAction(
1140
+ drivingClearSpeedLimitPinAction=DrivingClearSpeedLimitPinAction(
1141
+ pin=str(pin)
1142
+ )
1143
+ )
1144
+ )
1145
+ )
1146
+
1147
+ # speed_limit_clear_pin_admin doesnt require signing
1148
+
1149
+ async def speed_limit_deactivate(self, pin: str | int) -> dict[str, Any]:
1150
+ """Deactivates Speed Limit Mode."""
1151
+ return await self._sendInfotainment(
1152
+ Action(
1153
+ vehicleAction=VehicleAction(
1154
+ drivingSpeedLimitAction=DrivingSpeedLimitAction(
1155
+ activate=False, pin=str(pin)
1156
+ )
1157
+ )
1158
+ )
1159
+ )
1160
+
1161
+ async def speed_limit_set_limit(self, limit_mph: int) -> dict[str, Any]:
1162
+ """Sets the maximum speed allowed when Speed Limit Mode is active."""
1163
+ return await self._sendInfotainment(
1164
+ Action(
1165
+ vehicleAction=VehicleAction(
1166
+ drivingSetSpeedLimitAction=DrivingSetSpeedLimitAction(
1167
+ limit_mph=limit_mph
1168
+ )
1169
+ )
1170
+ )
1171
+ )
1172
+
1173
+ async def sun_roof_control(self, state: str | SunRoofCommand) -> dict[str, Any]:
1174
+ """Controls the panoramic sunroof on the Model S."""
1175
+ if isinstance(state, SunRoofCommand):
1176
+ state = state.value
1177
+ action = VehicleControlSunroofOpenCloseAction()
1178
+ match state:
1179
+ case "vent":
1180
+ action = VehicleControlSunroofOpenCloseAction(vent=Void())
1181
+ case "open":
1182
+ action = VehicleControlSunroofOpenCloseAction(open=Void())
1183
+ case "close":
1184
+ action = VehicleControlSunroofOpenCloseAction(close=Void())
1185
+
1186
+ return await self._sendInfotainment(
1187
+ Action(
1188
+ vehicleAction=VehicleAction(
1189
+ vehicleControlSunroofOpenCloseAction=action
1190
+ )
1191
+ )
1192
+ )
1193
+
1194
+ # take_drivenote doesnt require signing
1195
+
1196
+ async def trigger_homelink(
1197
+ self,
1198
+ token: str | None = None,
1199
+ lat: float | None = None,
1200
+ lon: float | None = None,
1201
+ ) -> dict[str, Any]:
1202
+ """Turns on HomeLink (used to open and close garage doors)."""
1203
+ action = VehicleControlTriggerHomelinkAction()
1204
+ if lat is not None and lon is not None:
1205
+ action.location.latitude = lat
1206
+ action.location.longitude = lon
1207
+ if token is not None:
1208
+ action.token = token
1209
+
1210
+ return await self._sendInfotainment(
1211
+ Action(
1212
+ vehicleAction=VehicleAction(vehicleControlTriggerHomelinkAction=action)
1213
+ )
1214
+ )
1215
+
1216
+ # upcoming_calendar_entries doesnt require signing
1217
+
1218
+ async def window_control(
1219
+ self,
1220
+ command: str | WindowCommand,
1221
+ lat: float | None = None,
1222
+ lon: float | None = None,
1223
+ ) -> dict[str, Any]:
1224
+ """Control the windows of a parked vehicle. Supported commands: vent and close. When closing, specify lat and lon of user to ensure they are within range of vehicle (unless this is an M3 platform vehicle)."""
1225
+ if isinstance(command, WindowCommand):
1226
+ command = command.value
1227
+
1228
+ if command == "vent":
1229
+ action = VehicleControlWindowAction(vent=Void())
1230
+ elif command == "close":
1231
+ action = VehicleControlWindowAction(close=Void())
1232
+ else:
1233
+ raise ValueError(f"Invalid window command: {command}")
1234
+
1235
+ return await self._sendInfotainment(
1236
+ Action(vehicleAction=VehicleAction(vehicleControlWindowAction=action))
1237
+ )
1238
+
1239
+ # drivers doesnt require signing
1240
+ # drivers_remove doesnt require signing
1241
+ # mobile_enabled
1242
+
1243
+ async def nearby_charging_sites(
1244
+ self,
1245
+ count: int | None = None,
1246
+ radius: int | None = None,
1247
+ detail: bool | None = None,
1248
+ ) -> dict[str, Any]:
1249
+ """Returns the charging sites near the current location of the vehicle."""
1250
+ action = GetNearbyChargingSites()
1251
+ if count is not None:
1252
+ action.count = count
1253
+ if radius is not None:
1254
+ action.radius = radius
1255
+ if detail is not None:
1256
+ action.include_meta_data = detail
1257
+
1258
+ return await self._sendInfotainment(
1259
+ Action(vehicleAction=VehicleAction(getNearbyChargingSites=action))
1260
+ )
1261
+
1262
+ # options doesnt require signing
1263
+ # recent_alerts doesnt require signing
1264
+ # release_notes doesnt require signing
1265
+ # service_data doesnt require signing
1266
+ # share_invites doesnt require signing
1267
+ # share_invites_create doesnt require signing
1268
+ # share_invites_redeem doesnt require signing
1269
+ # share_invites_revoke doesnt require signing
1270
+ # signed command doesnt require signing
1271
+ # vehicle doesnt require signing
1272
+ # vehicle_data doesnt require signing
1273
+ # wake_up doesnt require signing
1274
+ # warranty_details doesnt require signing
1275
+ # fleet_status doesnt require signing
1276
+
1277
+ async def fleet_telemetry_config_create(
1278
+ self, config: dict[str, Any]
1279
+ ) -> dict[str, Any]:
1280
+ """Configures fleet telemetry."""
1281
+ raise NotImplementedError
1282
+
1283
+ # fleet_telemetry_config_get doesnt require signing
1284
+ # fleet_telemetry_config_delete doesnt require signing