tesla-fleet-api 0.9.10__py3-none-any.whl → 1.0.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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))