tesla-fleet-api 0.9.10__py3-none-any.whl → 1.0.1__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 (54) hide show
  1. tesla_fleet_api/__init__.py +7 -22
  2. tesla_fleet_api/const.py +1 -225
  3. tesla_fleet_api/exceptions.py +117 -0
  4. tesla_fleet_api/tesla/__init__.py +11 -0
  5. tesla_fleet_api/tesla/bluetooth.py +38 -0
  6. tesla_fleet_api/{charging.py → tesla/charging.py} +1 -1
  7. tesla_fleet_api/{energy.py → tesla/energysite.py} +41 -33
  8. tesla_fleet_api/{teslafleetapi.py → tesla/fleet.py} +8 -53
  9. tesla_fleet_api/{teslafleetoauth.py → tesla/oauth.py} +3 -4
  10. tesla_fleet_api/{partner.py → tesla/partner.py} +1 -1
  11. tesla_fleet_api/tesla/tesla.py +52 -0
  12. tesla_fleet_api/{user.py → tesla/user.py} +1 -1
  13. tesla_fleet_api/tesla/vehicle/__init__.py +13 -0
  14. tesla_fleet_api/tesla/vehicle/bluetooth.py +219 -0
  15. tesla_fleet_api/{vehiclesigned.py → tesla/vehicle/commands.py} +321 -164
  16. tesla_fleet_api/{vehicle.py → tesla/vehicle/fleet.py} +173 -206
  17. tesla_fleet_api/tesla/vehicle/signed.py +56 -0
  18. tesla_fleet_api/tesla/vehicle/vehicle.py +19 -0
  19. tesla_fleet_api/tesla/vehicle/vehicles.py +46 -0
  20. tesla_fleet_api/teslemetry/__init__.py +5 -0
  21. tesla_fleet_api/{teslemetry.py → teslemetry/teslemetry.py} +16 -25
  22. tesla_fleet_api/teslemetry/vehicle.py +73 -0
  23. tesla_fleet_api/tessie/__init__.py +5 -0
  24. tesla_fleet_api/{tessie.py → tessie/tessie.py} +17 -9
  25. tesla_fleet_api/tessie/vehicle.py +41 -0
  26. {tesla_fleet_api-0.9.10.dist-info → tesla_fleet_api-1.0.1.dist-info}/METADATA +3 -2
  27. tesla_fleet_api-1.0.1.dist-info/RECORD +51 -0
  28. tesla_fleet_api/energyspecific.py +0 -125
  29. tesla_fleet_api/teslafleetopensource.py +0 -61
  30. tesla_fleet_api/vehiclespecific.py +0 -509
  31. tesla_fleet_api-0.9.10.dist-info/RECORD +0 -42
  32. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/__init__.py +0 -0
  33. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/__init__.pyi +0 -0
  34. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/car_server_pb2.py +0 -0
  35. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/car_server_pb2.pyi +0 -0
  36. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/common_pb2.py +0 -0
  37. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/common_pb2.pyi +0 -0
  38. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/errors_pb2.py +0 -0
  39. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/errors_pb2.pyi +0 -0
  40. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/keys_pb2.py +0 -0
  41. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/keys_pb2.pyi +0 -0
  42. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/managed_charging_pb2.py +0 -0
  43. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/managed_charging_pb2.pyi +0 -0
  44. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/signatures_pb2.py +0 -0
  45. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/signatures_pb2.pyi +0 -0
  46. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/universal_message_pb2.py +0 -0
  47. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/universal_message_pb2.pyi +0 -0
  48. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/vcsec_pb2.py +0 -0
  49. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/vcsec_pb2.pyi +0 -0
  50. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/vehicle_pb2.py +0 -0
  51. /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/vehicle_pb2.pyi +0 -0
  52. {tesla_fleet_api-0.9.10.dist-info → tesla_fleet_api-1.0.1.dist-info}/LICENSE +0 -0
  53. {tesla_fleet_api-0.9.10.dist-info → tesla_fleet_api-1.0.1.dist-info}/WHEEL +0 -0
  54. {tesla_fleet_api-0.9.10.dist-info → tesla_fleet_api-1.0.1.dist-info}/top_level.txt +0 -0
@@ -1,20 +1,28 @@
1
1
  from __future__ import annotations
2
- import base64
2
+ from abc import abstractmethod
3
+
4
+ import struct
3
5
  from random import randbytes
4
6
  from typing import Any, TYPE_CHECKING
5
7
  import time
6
- import struct
7
8
  import hmac
8
9
  import hashlib
9
10
  from cryptography.hazmat.primitives.asymmetric import ec
10
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
11
14
  from asyncio import Lock, sleep
12
15
 
13
- from tesla_fleet_api.pb2.errors_pb2 import GenericError_E
16
+ from ...exceptions import (
17
+ SIGNED_MESSAGE_INFORMATION_FAULTS,
18
+ TeslaFleetMessageFaultIncorrectEpoch,
19
+ TeslaFleetMessageFaultInvalidTokenOrCounter,
20
+ )
14
21
 
15
- from .exceptions import MESSAGE_FAULTS, SIGNED_MESSAGE_INFORMATION_FAULTS, TeslaFleetMessageFaultIncorrectEpoch, TeslaFleetMessageFaultInvalidTokenOrCounter
22
+ from .vehicle import Vehicle
16
23
 
17
- from .const import (
24
+
25
+ from ...const import (
18
26
  LOGGER,
19
27
  Trunk,
20
28
  ClimateKeeperMode,
@@ -22,18 +30,42 @@ from .const import (
22
30
  SunRoofCommand,
23
31
  WindowCommand,
24
32
  )
25
- from .vehiclespecific import VehicleSpecific
26
33
 
27
- from .pb2.universal_message_pb2 import (
28
- OPERATIONSTATUS_WAIT,
29
- OPERATIONSTATUS_ERROR,
30
- DOMAIN_VEHICLE_SECURITY,
34
+ # Protocol
35
+ from .proto.errors_pb2 import GenericError_E
36
+ from .proto.car_server_pb2 import (
37
+ Response,
38
+ )
39
+ from .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 .proto.universal_message_pb2 import (
31
55
  DOMAIN_INFOTAINMENT,
56
+ DOMAIN_VEHICLE_SECURITY,
57
+ OPERATIONSTATUS_ERROR,
58
+ OPERATIONSTATUS_WAIT,
59
+ Destination,
32
60
  Domain,
33
61
  RoutableMessage,
62
+ SessionInfoRequest,
34
63
  )
35
- from .pb2.car_server_pb2 import (
36
- Response,
64
+ from .proto.vcsec_pb2 import (
65
+ OPERATIONSTATUS_OK,
66
+ FromVCSECMessage,
67
+ )
68
+ from .proto.car_server_pb2 import (
37
69
  Action,
38
70
  MediaPlayAction,
39
71
  VehicleAction,
@@ -64,7 +96,7 @@ from .pb2.car_server_pb2 import (
64
96
  VehicleControlWindowAction,
65
97
  HvacBioweaponModeAction,
66
98
  AutoSeatClimateAction,
67
- # Ping,
99
+ Ping,
68
100
  ScheduledChargingAction,
69
101
  ScheduledDepartureAction,
70
102
  HvacClimateKeeperAction,
@@ -80,202 +112,171 @@ from .pb2.car_server_pb2 import (
80
112
  MediaPreviousTrack,
81
113
  MediaPreviousFavorite,
82
114
  )
83
- from .pb2.vehicle_pb2 import VehicleState
84
- from .pb2.vcsec_pb2 import (
85
- # SignedMessage_information_E,
86
- OPERATIONSTATUS_OK,
87
- FromVCSECMessage,
115
+ from .proto.vehicle_pb2 import VehicleState, ClimateState
116
+ from .proto.vcsec_pb2 import (
88
117
  UnsignedMessage,
89
118
  RKEAction_E,
90
119
  ClosureMoveRequest,
91
120
  ClosureMoveType_E,
92
121
  )
93
- from .pb2.signatures_pb2 import (
94
- SIGNATURE_TYPE_HMAC_PERSONALIZED,
95
- TAG_DOMAIN,
96
- TAG_SIGNATURE_TYPE,
97
- SessionInfo,
122
+ from .proto.signatures_pb2 import (
98
123
  HMAC_Personalized_Signature_Data,
99
- TAG_PERSONALIZATION,
100
- TAG_EPOCH,
101
- TAG_EXPIRES_AT,
102
- TAG_COUNTER,
103
- TAG_END,
104
124
  )
105
- from .pb2.common_pb2 import (
125
+ from .proto.common_pb2 import (
106
126
  Void,
107
127
  PreconditioningTimes,
108
128
  OffPeakChargingTimes,
109
- # ChargeSchedule,
110
- # PreconditionSchedule,
111
129
  )
112
130
 
113
131
  if TYPE_CHECKING:
114
- from .vehicle import Vehicle
132
+ from ..tesla import Tesla
133
+
134
+ # ENUMs to convert ints to proto typed ints
135
+ AutoSeatClimatePositions = (
136
+ AutoSeatClimateAction.AutoSeatPosition_FrontLeft,
137
+ AutoSeatClimateAction.AutoSeatPosition_FrontRight,
138
+ )
139
+
140
+ HvacSeatCoolerLevels = (
141
+ HvacSeatCoolerActions.HvacSeatCoolerLevel_Off,
142
+ HvacSeatCoolerActions.HvacSeatCoolerLevel_Low,
143
+ HvacSeatCoolerActions.HvacSeatCoolerLevel_Med,
144
+ HvacSeatCoolerActions.HvacSeatCoolerLevel_High,
145
+ )
146
+
147
+ HvacSeatCoolerPositions = (
148
+ HvacSeatCoolerActions.HvacSeatCoolerPosition_FrontLeft,
149
+ HvacSeatCoolerActions.HvacSeatCoolerPosition_FrontRight,
150
+ )
151
+
152
+ HvacClimateKeeperActions = (
153
+ HvacClimateKeeperAction.ClimateKeeperAction_Off,
154
+ HvacClimateKeeperAction.ClimateKeeperAction_On,
155
+ HvacClimateKeeperAction.ClimateKeeperAction_Dog,
156
+ HvacClimateKeeperAction.ClimateKeeperAction_Camp,
157
+ )
115
158
 
159
+ CopActivationTemps = (
160
+ ClimateState.CopActivationTemp.CopActivationTempLow,
161
+ ClimateState.CopActivationTemp.CopActivationTempMedium,
162
+ ClimateState.CopActivationTemp.CopActivationTempHigh,
163
+ )
116
164
 
117
165
  class Session:
118
166
  """A connect to a domain"""
119
167
 
168
+ domain: Domain
169
+ parent: Commands
120
170
  key: bytes
121
171
  counter: int
122
172
  epoch: bytes
123
173
  delta: int
174
+ sharedKey: bytes
124
175
  hmac: bytes
125
176
  publicKey: bytes
126
177
  lock: Lock
127
178
 
128
- def __init__(self):
179
+ def __init__(self, parent: Commands, domain: Domain):
180
+ self.parent = parent
181
+ self.domain = domain
129
182
  self.lock = Lock()
130
183
  self.counter = 0
184
+ self.ready = False
131
185
 
132
- def update(self, sessionInfo: SessionInfo, privateKey: ec.EllipticCurvePrivateKey):
186
+ def update(self, sessionInfo: SessionInfo):
133
187
  """Update the session with new information"""
188
+
134
189
  self.counter = sessionInfo.counter
135
190
  self.epoch = sessionInfo.epoch
136
191
  self.delta = int(time.time()) - sessionInfo.clock_time
137
- if (self.publicKey != sessionInfo.publicKey):
192
+ if (not self.ready or self.publicKey != sessionInfo.publicKey):
138
193
  self.publicKey = sessionInfo.publicKey
139
- self.key = hashlib.sha1(
140
- privateKey.exchange(
141
- ec.ECDH(),
142
- ec.EllipticCurvePublicKey.from_encoded_point(
143
- ec.SECP256R1(), self.publicKey
144
- ),
145
- ),
146
- ).digest()[:16]
147
- self.hmac = hmac.new(
148
- self.key, "authenticated command".encode(), hashlib.sha256
149
- ).digest()
194
+ self.sharedKey = self.parent.shared_key(sessionInfo.publicKey)
195
+ self.hmac = hmac.new(self.sharedKey, "authenticated command".encode(), hashlib.sha256).digest()
196
+ self.ready = True
150
197
 
151
- def get(self) -> HMAC_Personalized_Signature_Data:
198
+ def hmac_personalized(self) -> HMAC_Personalized_Signature_Data:
152
199
  """Sign a command and return session metadata"""
153
- self.counter = self.counter+1
200
+ self.counter += 1
154
201
  return HMAC_Personalized_Signature_Data(
155
202
  epoch=self.epoch,
156
203
  counter=self.counter,
157
204
  expires_at=int(time.time()) - self.delta + 10,
158
205
  )
159
206
 
207
+ def aes_gcm_personalized(self) -> AES_GCM_Personalized_Signature_Data:
208
+ """Sign a command and return session metadata"""
209
+ self.counter += 1
210
+ return AES_GCM_Personalized_Signature_Data(
211
+ epoch=self.epoch,
212
+ nonce=randbytes(12),
213
+ counter=self.counter,
214
+ expires_at=int(time.time()) - self.delta + 10,
215
+ )
216
+
160
217
 
161
- class VehicleSigned(VehicleSpecific):
218
+ class Commands(Vehicle):
162
219
  """Class describing the Tesla Fleet API vehicle endpoints and commands for a specific vehicle with command signing."""
163
220
 
164
221
  private_key: ec.EllipticCurvePrivateKey
165
222
  _public_key: bytes
166
223
  _from_destination: bytes
167
224
  _sessions: dict[int, Session]
225
+ _require_keys = True
226
+ _auth_method: str
168
227
 
169
228
  def __init__(
170
- self, parent: Vehicle, vin: str, key: ec.EllipticCurvePrivateKey | None = None
229
+ self, parent: Tesla, vin: str, private_key: ec.EllipticCurvePrivateKey | None = None, public_key: bytes | None = None
171
230
  ):
172
231
  super().__init__(parent, vin)
173
- if key:
174
- self.private_key = key
175
- elif parent._parent.private_key:
176
- self.private_key = parent._parent.private_key
177
- else:
178
- raise ValueError("No private key.")
179
232
 
180
- self._public_key = self.private_key.public_key().public_bytes(
181
- encoding=Encoding.X962, format=PublicFormat.UncompressedPoint
182
- )
183
233
  self._from_destination = randbytes(16)
184
- self._sessions = {}
185
-
186
- async def _send(self, msg: RoutableMessage) -> RoutableMessage:
187
- """Serialize a message and send to the signed command endpoint."""
188
-
189
- async with self._sessions[msg.to_destination.domain].lock:
190
- resp = await self.signed_command(
191
- base64.b64encode(msg.SerializeToString()).decode()
192
- )
193
-
194
- resp_msg = RoutableMessage.FromString(base64.b64decode(resp["response"]))
195
-
196
- # Check UUID?
197
- # Check RoutingAdress?
198
-
199
- if resp_msg.session_info:
200
- self._sessions[resp_msg.from_destination.domain].update(
201
- SessionInfo.FromString(resp_msg.session_info), self.private_key
202
- )
203
-
204
- if resp_msg.signedMessageStatus.signed_message_fault:
205
- raise MESSAGE_FAULTS[resp_msg.signedMessageStatus.signed_message_fault]
206
-
207
- return resp_msg
208
-
209
- async def _handshake(self, domain: Domain) -> Session:
210
- """Perform a handshake with the vehicle."""
211
- if session := self._sessions.get(domain):
212
- return session
213
- self._sessions[domain] = Session()
214
-
215
- LOGGER.debug(f"Handshake with domain {Domain.Name(domain)}")
216
- msg = RoutableMessage()
217
- msg.to_destination.domain = domain
218
- msg.from_destination.routing_address = self._from_destination
219
- msg.session_info_request.public_key = self._public_key
220
- msg.uuid = randbytes(16)
221
-
222
- # Send handshake message
223
- await self._send(msg)
224
-
225
- return self._sessions[domain]
226
-
227
- async def _sendVehicleSecurity(self, command: UnsignedMessage) -> dict[str, Any]:
228
- """Sign and send a message to Infotainment computer."""
229
- return await self._sign(DOMAIN_VEHICLE_SECURITY, command.SerializeToString())
230
-
231
- async def _sendInfotainment(self, command: Action) -> dict[str, Any]:
232
- """Sign and send a message to Infotainment computer."""
233
- return await self._sign(DOMAIN_INFOTAINMENT, command.SerializeToString())
234
-
235
- async def _sign(
236
- self, domain: Domain, command: bytes, attempt: int = 1
237
- ) -> dict[str, Any]:
238
- """Send a signed message to the vehicle."""
239
- LOGGER.debug(f"Sending to domain {Domain.Name(domain)}")
234
+ self._sessions = {
235
+ Domain.DOMAIN_VEHICLE_SECURITY: Session(self, Domain.DOMAIN_VEHICLE_SECURITY),
236
+ Domain.DOMAIN_INFOTAINMENT: Session(self, Domain.DOMAIN_INFOTAINMENT),
237
+ }
238
+
239
+ if(self._require_keys):
240
+ if private_key:
241
+ self.private_key = private_key
242
+ elif parent.private_key:
243
+ self.private_key = parent.private_key
244
+ else:
245
+ raise ValueError("No private key.")
246
+
247
+ self._public_key = public_key or self.private_key.public_key().public_bytes(
248
+ encoding=Encoding.X962, format=PublicFormat.UncompressedPoint
249
+ )
240
250
 
241
- session = await self._handshake(domain)
242
- hmac_personalized = session.get()
243
251
 
244
- msg = RoutableMessage()
245
- msg.to_destination.domain = domain
246
- msg.from_destination.routing_address = self._from_destination
247
- msg.protobuf_message_as_bytes = command
248
- msg.uuid = randbytes(16)
252
+ def shared_key(self, vehicleKey: bytes) -> bytes:
253
+ exchange = self.private_key.exchange(
254
+ ec.ECDH(),
255
+ ec.EllipticCurvePublicKey.from_encoded_point(
256
+ ec.SECP256R1(), vehicleKey
257
+ ),
258
+ )
259
+ return hashlib.sha1(exchange).digest()[:16]
249
260
 
250
- metadata = bytes([
251
- TAG_SIGNATURE_TYPE,
252
- 1,
253
- SIGNATURE_TYPE_HMAC_PERSONALIZED,
254
- TAG_DOMAIN,
255
- 1,
256
- domain,
257
- TAG_PERSONALIZATION,
258
- 17,
259
- *self.vin.encode(),
260
- TAG_EPOCH,
261
- len(hmac_personalized.epoch),
262
- *hmac_personalized.epoch,
263
- TAG_EXPIRES_AT,
264
- 4,
265
- *struct.pack(">I", hmac_personalized.expires_at),
266
- TAG_COUNTER,
267
- 4,
268
- *struct.pack(">I", hmac_personalized.counter),
269
- TAG_END,
270
- ])
271
261
 
272
- hmac_personalized.tag = hmac.new(
273
- session.hmac, metadata + command, hashlib.sha256
274
- ).digest()
262
+ @abstractmethod
263
+ async def _send(self, msg: RoutableMessage) -> RoutableMessage:
264
+ """Transmit the message to the vehicle."""
265
+ raise NotImplementedError
275
266
 
276
- # I think this whole section could be improved
277
- msg.signature_data.HMAC_Personalized_data.CopyFrom(hmac_personalized)
278
- msg.signature_data.signer_identity.public_key = self._public_key
267
+ @abstractmethod
268
+ async def _command(self, domain: Domain, command: bytes, attempt: int = 0) -> dict[str, Any]:
269
+ """Serialize a message and send to the signed command endpoint."""
270
+ session = self._sessions[domain]
271
+ if not session.ready:
272
+ await self._handshake(domain)
273
+
274
+ if self._auth_method == "hmac":
275
+ msg = await self._commandHmac(session, command)
276
+ elif self._auth_method == "aes":
277
+ msg = await self._commandAes(session, command)
278
+ else:
279
+ raise ValueError(f"Unknown auth method: {self._auth_method}")
279
280
 
280
281
  try:
281
282
  resp = await self._send(msg)
@@ -287,7 +288,7 @@ class VehicleSigned(VehicleSpecific):
287
288
  if attempt > 3:
288
289
  # We tried 3 times, give up, raise the error
289
290
  raise e
290
- return await self._sign(domain, command, attempt)
291
+ return await self._command(domain, command, attempt)
291
292
 
292
293
  if resp.signedMessageStatus.operation_status == OPERATIONSTATUS_WAIT:
293
294
  attempt += 1
@@ -296,7 +297,7 @@ class VehicleSigned(VehicleSpecific):
296
297
  return {"response": {"result": False, "reason": "Too many retries"}}
297
298
  async with session.lock:
298
299
  await sleep(2)
299
- return await self._sign(domain, command, attempt)
300
+ return await self._command(domain, command, attempt)
300
301
 
301
302
  if resp.HasField("protobuf_message_as_bytes"):
302
303
  if(resp.from_destination.domain == DOMAIN_VEHICLE_SECURITY):
@@ -319,7 +320,7 @@ class VehicleSigned(VehicleSpecific):
319
320
  return {"response": {"result": False, "reason": "Too many retries"}}
320
321
  async with session.lock:
321
322
  await sleep(2)
322
- return await self._sign(domain, command, attempt)
323
+ return await self._command(domain, command, attempt)
323
324
  elif vcsec.commandStatus.operationStatus == OPERATIONSTATUS_ERROR:
324
325
  if(resp.HasField("signedMessageStatus")):
325
326
  raise SIGNED_MESSAGE_INFORMATION_FAULTS[vcsec.commandStatus.signedMessageStatus.signedMessageInformation]
@@ -345,6 +346,155 @@ class VehicleSigned(VehicleSpecific):
345
346
 
346
347
  return {"response": {"result": True, "reason": ""}}
347
348
 
349
+ async def _commandHmac(self, session: Session, command: bytes, attempt: int = 1) -> RoutableMessage:
350
+ """Create a signed message."""
351
+ LOGGER.debug(f"Sending HMAC to domain {Domain.Name(session.domain)}")
352
+
353
+ hmac_personalized = session.hmac_personalized()
354
+
355
+ metadata = bytes([
356
+ TAG_SIGNATURE_TYPE,
357
+ 1,
358
+ SIGNATURE_TYPE_HMAC_PERSONALIZED,
359
+ TAG_DOMAIN,
360
+ 1,
361
+ session.domain,
362
+ TAG_PERSONALIZATION,
363
+ 17,
364
+ *self.vin.encode(),
365
+ TAG_EPOCH,
366
+ len(hmac_personalized.epoch),
367
+ *hmac_personalized.epoch,
368
+ TAG_EXPIRES_AT,
369
+ 4,
370
+ *struct.pack(">I", hmac_personalized.expires_at),
371
+ TAG_COUNTER,
372
+ 4,
373
+ *struct.pack(">I", hmac_personalized.counter),
374
+ TAG_END,
375
+ ])
376
+
377
+ hmac_personalized.tag = hmac.new(
378
+ session.hmac, metadata + command, hashlib.sha256
379
+ ).digest()
380
+
381
+ return RoutableMessage(
382
+ to_destination=Destination(
383
+ domain=session.domain,
384
+ ),
385
+ from_destination=Destination(
386
+ routing_address=self._from_destination
387
+ ),
388
+ protobuf_message_as_bytes=command,
389
+ uuid=randbytes(16),
390
+ signature_data=SignatureData(
391
+ signer_identity=KeyIdentity(
392
+ public_key=self._public_key
393
+ ),
394
+ HMAC_Personalized_data=hmac_personalized,
395
+ )
396
+ )
397
+
398
+ async def _commandAes(self, session: Session, command: bytes, attempt: int = 1) -> RoutableMessage:
399
+ """Create an encrypted message."""
400
+ LOGGER.debug(f"Sending AES to domain {Domain.Name(session.domain)}")
401
+
402
+ aes_personalized = session.aes_gcm_personalized()
403
+
404
+ metadata = bytes([
405
+ TAG_SIGNATURE_TYPE,
406
+ 1,
407
+ SIGNATURE_TYPE_AES_GCM_PERSONALIZED,
408
+ TAG_DOMAIN,
409
+ 1,
410
+ session.domain,
411
+ TAG_PERSONALIZATION,
412
+ 17,
413
+ *self.vin.encode(),
414
+ TAG_EPOCH,
415
+ len(aes_personalized.epoch),
416
+ *aes_personalized.epoch,
417
+ TAG_EXPIRES_AT,
418
+ 4,
419
+ *struct.pack(">I", aes_personalized.expires_at),
420
+ TAG_COUNTER,
421
+ 4,
422
+ *struct.pack(">I", aes_personalized.counter),
423
+ TAG_END,
424
+ ])
425
+
426
+ aad = Hash(SHA256())
427
+ aad.update(metadata)
428
+
429
+ aesgcm = AESGCM(session.sharedKey)
430
+ ct = aesgcm.encrypt(aes_personalized.nonce, command, aad.finalize())
431
+
432
+ aes_personalized.tag = ct[-16:]
433
+
434
+ # I think this whole section could be improved
435
+ return RoutableMessage(
436
+ to_destination=Destination(
437
+ domain=session.domain,
438
+ ),
439
+ from_destination=Destination(
440
+ routing_address=self._from_destination
441
+ ),
442
+ protobuf_message_as_bytes=ct[:-16],
443
+ uuid=randbytes(16),
444
+ signature_data=SignatureData(
445
+ signer_identity=KeyIdentity(
446
+ public_key=self._public_key
447
+ ),
448
+ AES_GCM_Personalized_data=aes_personalized,
449
+ )
450
+ )
451
+
452
+
453
+ async def _sendVehicleSecurity(self, command: UnsignedMessage) -> dict[str, Any]:
454
+ """Sign and send a message to Infotainment computer."""
455
+ return await self._command(Domain.DOMAIN_VEHICLE_SECURITY, command.SerializeToString())
456
+
457
+ async def _sendInfotainment(self, command: Action) -> dict[str, Any]:
458
+ """Sign and send a message to Infotainment computer."""
459
+ return await self._command(Domain.DOMAIN_INFOTAINMENT, command.SerializeToString())
460
+
461
+ async def handshakeVehicleSecurity(self) -> None:
462
+ """Perform a handshake with the vehicle security domain."""
463
+ await self._handshake(Domain.DOMAIN_VEHICLE_SECURITY)
464
+
465
+ async def handshakeInfotainment(self) -> None:
466
+ """Perform a handshake with the infotainment domain."""
467
+ await self._handshake(Domain.DOMAIN_INFOTAINMENT)
468
+
469
+ async def _handshake(self, domain: Domain) -> None:
470
+ """Perform a handshake with the vehicle."""
471
+
472
+ LOGGER.debug(f"Handshake with domain {Domain.Name(domain)}")
473
+ msg = RoutableMessage(
474
+ to_destination=Destination(
475
+ domain=domain,
476
+ ),
477
+ from_destination=Destination(
478
+ routing_address=self._from_destination
479
+ ),
480
+ session_info_request=SessionInfoRequest(
481
+ public_key=self._public_key
482
+ ),
483
+ uuid=randbytes(16)
484
+ )
485
+
486
+ await self._send(msg)
487
+
488
+ async def ping(self) -> dict[str, Any]:
489
+ """Ping the vehicle."""
490
+ return await self._sendInfotainment(
491
+ Action(
492
+ vehicleAction=VehicleAction(
493
+ ping=Ping(ping_id=0)
494
+ )
495
+ )
496
+ )
497
+
348
498
  async def actuate_trunk(self, which_trunk: Trunk | str) -> dict[str, Any]:
349
499
  """Controls the front or rear trunk."""
350
500
  if which_trunk == Trunk.FRONT:
@@ -363,6 +513,7 @@ class VehicleSigned(VehicleSpecific):
363
513
  )
364
514
  )
365
515
  )
516
+ raise ValueError("Invalid trunk.")
366
517
 
367
518
  async def adjust_volume(self, volume: float) -> dict[str, Any]:
368
519
  """Adjusts vehicle media playback volume."""
@@ -468,7 +619,7 @@ class VehicleSigned(VehicleSpecific):
468
619
  )
469
620
  )
470
621
 
471
- async def clear_pin_to_drive_admin(self, pin: str):
622
+ async def clear_pin_to_drive_admin(self, pin: str | None = None):
472
623
  """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."""
473
624
  return await self._sendInfotainment(
474
625
  Action(
@@ -588,6 +739,7 @@ class VehicleSigned(VehicleSpecific):
588
739
  # navigation_gps_request doesnt require signing
589
740
  # navigation_request doesnt require signing
590
741
  # navigation_sc_request doesnt require signing
742
+ #
591
743
 
592
744
  async def remote_auto_seat_climate_request(
593
745
  self, auto_seat_position: int, auto_climate_on: bool
@@ -601,7 +753,7 @@ class VehicleSigned(VehicleSpecific):
601
753
  autoSeatClimateAction=AutoSeatClimateAction(
602
754
  carseat=[
603
755
  AutoSeatClimateAction.CarSeat(
604
- on=auto_climate_on, seat_position=auto_seat_position
756
+ on=auto_climate_on, seat_position=AutoSeatClimatePositions[auto_seat_position]
605
757
  )
606
758
  ]
607
759
  )
@@ -612,6 +764,8 @@ class VehicleSigned(VehicleSpecific):
612
764
  # remote_auto_steering_wheel_heat_climate_request has no protobuf
613
765
 
614
766
  # remote_boombox not implemented
767
+ #
768
+
615
769
 
616
770
  async def remote_seat_cooler_request(
617
771
  self, seat_position: int, seat_cooler_level: int
@@ -631,8 +785,8 @@ class VehicleSigned(VehicleSpecific):
631
785
  hvacSeatCoolerActions=HvacSeatCoolerActions(
632
786
  hvacSeatCoolerAction=[
633
787
  HvacSeatCoolerActions.HvacSeatCoolerAction(
634
- seat_cooler_level=seat_cooler_level + 1,
635
- seat_position=seat_position,
788
+ seat_cooler_level=HvacSeatCoolerLevels[seat_cooler_level],
789
+ seat_position=HvacSeatCoolerPositions[seat_position],
636
790
  )
637
791
  ]
638
792
  )
@@ -817,11 +971,12 @@ class VehicleSigned(VehicleSpecific):
817
971
  """Enables climate keeper mode."""
818
972
  if isinstance(climate_keeper_mode, ClimateKeeperMode):
819
973
  climate_keeper_mode = climate_keeper_mode.value
974
+
820
975
  return await self._sendInfotainment(
821
976
  Action(
822
977
  vehicleAction=VehicleAction(
823
978
  hvacClimateKeeperAction=HvacClimateKeeperAction(
824
- ClimateKeeperAction=climate_keeper_mode,
979
+ ClimateKeeperAction=HvacClimateKeeperActions[climate_keeper_mode],
825
980
  # manual_override
826
981
  )
827
982
  )
@@ -837,7 +992,7 @@ class VehicleSigned(VehicleSpecific):
837
992
  return await self._sendInfotainment(
838
993
  Action(
839
994
  vehicleAction=VehicleAction(
840
- setCopTempAction=SetCopTempAction(copActivationTemp=cop_temp + 1)
995
+ setCopTempAction=SetCopTempAction(copActivationTemp=CopActivationTemps[cop_temp])
841
996
  )
842
997
  )
843
998
  )
@@ -876,7 +1031,7 @@ class VehicleSigned(VehicleSpecific):
876
1031
  Action(
877
1032
  vehicleAction=VehicleAction(
878
1033
  scheduledChargingAction=ScheduledChargingAction(
879
- enable=enable, charging_time=time
1034
+ enabled=enable, charging_time=time
880
1035
  )
881
1036
  )
882
1037
  )
@@ -895,14 +1050,14 @@ class VehicleSigned(VehicleSpecific):
895
1050
  """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)."""
896
1051
 
897
1052
  if preconditioning_weekdays_only:
898
- preconditioning_times = PreconditioningTimes(weekdays=Void)
1053
+ preconditioning_times = PreconditioningTimes(weekdays=Void())
899
1054
  else:
900
- preconditioning_times = PreconditioningTimes(all_week=Void)
1055
+ preconditioning_times = PreconditioningTimes(all_week=Void())
901
1056
 
902
1057
  if off_peak_charging_weekdays_only:
903
- off_peak_charging_times = OffPeakChargingTimes(weekdays=Void)
1058
+ off_peak_charging_times = OffPeakChargingTimes(weekdays=Void())
904
1059
  else:
905
- off_peak_charging_times = OffPeakChargingTimes(all_week=Void)
1060
+ off_peak_charging_times = OffPeakChargingTimes(all_week=Void())
906
1061
 
907
1062
  return await self._sendInfotainment(
908
1063
  Action(
@@ -962,7 +1117,7 @@ class VehicleSigned(VehicleSpecific):
962
1117
  return await self._sendInfotainment(
963
1118
  Action(
964
1119
  vehicleAction=VehicleAction(
965
- setVehicleNameAction=SetVehicleNameAction(vehicle_name=vehicle_name)
1120
+ setVehicleNameAction=SetVehicleNameAction(vehicleName=vehicle_name)
966
1121
  )
967
1122
  )
968
1123
  )
@@ -1076,6 +1231,8 @@ class VehicleSigned(VehicleSpecific):
1076
1231
  action = VehicleControlWindowAction(vent=Void())
1077
1232
  elif command == "close":
1078
1233
  action = VehicleControlWindowAction(close=Void())
1234
+ else:
1235
+ raise ValueError(f"Invalid window command: {command}")
1079
1236
 
1080
1237
  return await self._sendInfotainment(
1081
1238
  Action(vehicleAction=VehicleAction(vehicleControlWindowAction=action))