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.
- 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))
|