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.
- tesla_fleet_api/__init__.py +7 -22
- tesla_fleet_api/const.py +1 -225
- tesla_fleet_api/exceptions.py +117 -0
- tesla_fleet_api/tesla/__init__.py +11 -0
- tesla_fleet_api/tesla/bluetooth.py +38 -0
- tesla_fleet_api/{charging.py → tesla/charging.py} +1 -1
- tesla_fleet_api/{energy.py → tesla/energysite.py} +41 -33
- tesla_fleet_api/{teslafleetapi.py → tesla/fleet.py} +8 -53
- tesla_fleet_api/{teslafleetoauth.py → tesla/oauth.py} +3 -4
- tesla_fleet_api/{partner.py → tesla/partner.py} +1 -1
- tesla_fleet_api/tesla/tesla.py +52 -0
- tesla_fleet_api/{user.py → tesla/user.py} +1 -1
- tesla_fleet_api/tesla/vehicle/__init__.py +13 -0
- tesla_fleet_api/tesla/vehicle/bluetooth.py +219 -0
- tesla_fleet_api/{vehiclesigned.py → tesla/vehicle/commands.py} +321 -164
- tesla_fleet_api/{vehicle.py → tesla/vehicle/fleet.py} +173 -206
- tesla_fleet_api/tesla/vehicle/signed.py +56 -0
- tesla_fleet_api/tesla/vehicle/vehicle.py +19 -0
- tesla_fleet_api/tesla/vehicle/vehicles.py +46 -0
- tesla_fleet_api/teslemetry/__init__.py +5 -0
- tesla_fleet_api/{teslemetry.py → teslemetry/teslemetry.py} +16 -25
- tesla_fleet_api/teslemetry/vehicle.py +73 -0
- tesla_fleet_api/tessie/__init__.py +5 -0
- tesla_fleet_api/{tessie.py → tessie/tessie.py} +17 -9
- tesla_fleet_api/tessie/vehicle.py +41 -0
- {tesla_fleet_api-0.9.10.dist-info → tesla_fleet_api-1.0.1.dist-info}/METADATA +3 -2
- tesla_fleet_api-1.0.1.dist-info/RECORD +51 -0
- tesla_fleet_api/energyspecific.py +0 -125
- tesla_fleet_api/teslafleetopensource.py +0 -61
- tesla_fleet_api/vehiclespecific.py +0 -509
- tesla_fleet_api-0.9.10.dist-info/RECORD +0 -42
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/__init__.py +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/__init__.pyi +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/car_server_pb2.py +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/car_server_pb2.pyi +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/common_pb2.py +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/common_pb2.pyi +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/errors_pb2.py +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/errors_pb2.pyi +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/keys_pb2.py +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/keys_pb2.pyi +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/managed_charging_pb2.py +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/managed_charging_pb2.pyi +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/signatures_pb2.py +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/signatures_pb2.pyi +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/universal_message_pb2.py +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/universal_message_pb2.pyi +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/vcsec_pb2.py +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/vcsec_pb2.pyi +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/vehicle_pb2.py +0 -0
- /tesla_fleet_api/{pb2 → tesla/vehicle/proto}/vehicle_pb2.pyi +0 -0
- {tesla_fleet_api-0.9.10.dist-info → tesla_fleet_api-1.0.1.dist-info}/LICENSE +0 -0
- {tesla_fleet_api-0.9.10.dist-info → tesla_fleet_api-1.0.1.dist-info}/WHEEL +0 -0
- {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
|
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
|
16
|
+
from ...exceptions import (
|
17
|
+
SIGNED_MESSAGE_INFORMATION_FAULTS,
|
18
|
+
TeslaFleetMessageFaultIncorrectEpoch,
|
19
|
+
TeslaFleetMessageFaultInvalidTokenOrCounter,
|
20
|
+
)
|
14
21
|
|
15
|
-
from .
|
22
|
+
from .vehicle import Vehicle
|
16
23
|
|
17
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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 .
|
36
|
-
|
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
|
-
|
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 .
|
84
|
-
from .
|
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 .
|
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 .
|
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
|
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
|
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.
|
140
|
-
|
141
|
-
|
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
|
198
|
+
def hmac_personalized(self) -> HMAC_Personalized_Signature_Data:
|
152
199
|
"""Sign a command and return session metadata"""
|
153
|
-
self.counter
|
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
|
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:
|
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
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
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
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
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
|
-
|
273
|
-
|
274
|
-
|
262
|
+
@abstractmethod
|
263
|
+
async def _send(self, msg: RoutableMessage) -> RoutableMessage:
|
264
|
+
"""Transmit the message to the vehicle."""
|
265
|
+
raise NotImplementedError
|
275
266
|
|
276
|
-
|
277
|
-
|
278
|
-
|
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.
|
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.
|
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.
|
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
|
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
|
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
|
-
|
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(
|
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))
|