tesla-fleet-api 1.0.0__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 (35) hide show
  1. tesla_fleet_api/const.py +1 -1
  2. tesla_fleet_api/tesla/bluetooth.py +6 -1
  3. tesla_fleet_api/tesla/vehicle/__init__.py +13 -0
  4. tesla_fleet_api/tesla/vehicle/bluetooth.py +219 -0
  5. tesla_fleet_api/tesla/vehicle/commands.py +1286 -0
  6. tesla_fleet_api/tesla/vehicle/fleet.py +847 -0
  7. tesla_fleet_api/tesla/vehicle/proto/__init__.py +0 -0
  8. tesla_fleet_api/tesla/vehicle/proto/__init__.pyi +9 -0
  9. tesla_fleet_api/tesla/vehicle/proto/car_server_pb2.py +175 -0
  10. tesla_fleet_api/tesla/vehicle/proto/car_server_pb2.pyi +904 -0
  11. tesla_fleet_api/tesla/vehicle/proto/common_pb2.py +33 -0
  12. tesla_fleet_api/tesla/vehicle/proto/common_pb2.pyi +130 -0
  13. tesla_fleet_api/tesla/vehicle/proto/errors_pb2.py +17 -0
  14. tesla_fleet_api/tesla/vehicle/proto/errors_pb2.pyi +32 -0
  15. tesla_fleet_api/tesla/vehicle/proto/keys_pb2.py +15 -0
  16. tesla_fleet_api/tesla/vehicle/proto/keys_pb2.pyi +21 -0
  17. tesla_fleet_api/tesla/vehicle/proto/managed_charging_pb2.py +15 -0
  18. tesla_fleet_api/tesla/vehicle/proto/managed_charging_pb2.pyi +17 -0
  19. tesla_fleet_api/tesla/vehicle/proto/signatures_pb2.py +35 -0
  20. tesla_fleet_api/tesla/vehicle/proto/signatures_pb2.pyi +152 -0
  21. tesla_fleet_api/tesla/vehicle/proto/universal_message_pb2.py +30 -0
  22. tesla_fleet_api/tesla/vehicle/proto/universal_message_pb2.pyi +148 -0
  23. tesla_fleet_api/tesla/vehicle/proto/vcsec_pb2.py +79 -0
  24. tesla_fleet_api/tesla/vehicle/proto/vcsec_pb2.pyi +482 -0
  25. tesla_fleet_api/tesla/vehicle/proto/vehicle_pb2.py +125 -0
  26. tesla_fleet_api/tesla/vehicle/proto/vehicle_pb2.pyi +1183 -0
  27. tesla_fleet_api/tesla/vehicle/signed.py +56 -0
  28. tesla_fleet_api/tesla/vehicle/vehicle.py +19 -0
  29. tesla_fleet_api/tesla/vehicle/vehicles.py +46 -0
  30. {tesla_fleet_api-1.0.0.dist-info → tesla_fleet_api-1.0.1.dist-info}/METADATA +1 -1
  31. tesla_fleet_api-1.0.1.dist-info/RECORD +51 -0
  32. tesla_fleet_api-1.0.0.dist-info/RECORD +0 -24
  33. {tesla_fleet_api-1.0.0.dist-info → tesla_fleet_api-1.0.1.dist-info}/LICENSE +0 -0
  34. {tesla_fleet_api-1.0.0.dist-info → tesla_fleet_api-1.0.1.dist-info}/WHEEL +0 -0
  35. {tesla_fleet_api-1.0.0.dist-info → tesla_fleet_api-1.0.1.dist-info}/top_level.txt +0 -0
tesla_fleet_api/const.py CHANGED
@@ -3,7 +3,7 @@
3
3
  from enum import Enum
4
4
  import logging
5
5
 
6
- VERSION = "1.0.0"
6
+ VERSION = "1.0.1"
7
7
  LOGGER = logging.getLogger(__package__)
8
8
  SERVERS = {
9
9
  "na": "https://fleet-api.prd.na.vn.cloud.tesla.com",
@@ -1,5 +1,6 @@
1
1
  """Bluetooth only interface."""
2
2
 
3
+ import hashlib
3
4
  import re
4
5
  from .tesla import Tesla
5
6
  from .vehicle.bluetooth import VehicleBluetooth
@@ -16,7 +17,11 @@ class TeslaBluetooth(Tesla):
16
17
 
17
18
  def valid_name(self, name: str) -> bool:
18
19
  """Check if a BLE device name is a valid Tesla vehicle."""
19
- return bool(re.match("^S[a-f0-9]{16}[A-F]$", name))
20
+ return bool(re.match("^S[a-f0-9]{16}[CDRP]$", name))
21
+
22
+ def get_name(self, vin: str) -> str:
23
+ """Get the name of a vehicle."""
24
+ return "S" + hashlib.sha1(vin.encode('utf-8')).hexdigest()[:16] + "C"
20
25
 
21
26
  class Vehicles(dict[str, VehicleBluetooth]):
22
27
  """Class containing and creating vehicles."""
@@ -0,0 +1,13 @@
1
+ """Tesla Fleet API classes."""
2
+
3
+ from .vehicles import Vehicles
4
+ from .fleet import VehicleFleet
5
+ from .bluetooth import VehicleBluetooth
6
+ from .signed import VehicleSigned
7
+
8
+ __all__ = [
9
+ "Vehicles",
10
+ "VehicleFleet",
11
+ "VehicleBluetooth",
12
+ "VehicleSigned",
13
+ ]
@@ -0,0 +1,219 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from asyncio import Future, get_running_loop
5
+ from typing import TYPE_CHECKING
6
+ from google.protobuf.message import DecodeError
7
+
8
+ from bleak import BleakClient, BleakScanner
9
+ from bleak.backends.characteristic import BleakGATTCharacteristic
10
+ from bleak.backends.device import BLEDevice
11
+ from cryptography.hazmat.primitives.asymmetric import ec
12
+
13
+ from tesla_fleet_api.tesla.vehicle.proto.keys_pb2 import Role
14
+
15
+ from .commands import Commands
16
+
17
+ from ...const import (
18
+ LOGGER,
19
+ )
20
+ from ...exceptions import (
21
+ MESSAGE_FAULTS,
22
+ WHITELIST_OPERATION_STATUS,
23
+ )
24
+
25
+ # Protocol
26
+ from .proto.car_server_pb2 import (
27
+ Response,
28
+ )
29
+ from .proto.signatures_pb2 import (
30
+ SessionInfo,
31
+ )
32
+ from .proto.universal_message_pb2 import (
33
+ Destination,
34
+ Domain,
35
+ RoutableMessage,
36
+ )
37
+ from .proto.vcsec_pb2 import (
38
+ FromVCSECMessage,
39
+ KeyFormFactor,
40
+ KeyMetadata,
41
+ PermissionChange,
42
+ PublicKey,
43
+ UnsignedMessage,
44
+ WhitelistOperation,
45
+
46
+ )
47
+
48
+ SERVICE_UUID = "00000211-b2d1-43f0-9b88-960cebf8b91e"
49
+ WRITE_UUID = "00000212-b2d1-43f0-9b88-960cebf8b91e"
50
+ READ_UUID = "00000213-b2d1-43f0-9b88-960cebf8b91e"
51
+ VERSION_UUID = "00000214-b2d1-43f0-9b88-960cebf8b91e"
52
+
53
+ if TYPE_CHECKING:
54
+ from ..tesla import Tesla
55
+
56
+ def prependLength(message: bytes) -> bytearray:
57
+ """Prepend a 2-byte length to the payload."""
58
+ return bytearray([len(message) >> 8, len(message) & 0xFF]) + message
59
+
60
+ class VehicleBluetooth(Commands):
61
+ """Class describing the Tesla Fleet API vehicle endpoints and commands for a specific vehicle with command signing."""
62
+
63
+ ble_name: str
64
+ client: BleakClient
65
+ _device: BLEDevice
66
+ _futures: dict[Domain, Future]
67
+ _ekey: ec.EllipticCurvePublicKey
68
+ _recv: bytearray = bytearray()
69
+ _recv_len: int = 0
70
+ _auth_method = "aes"
71
+
72
+ def __init__(
73
+ self, parent: Tesla, vin: str, key: ec.EllipticCurvePrivateKey | None = None
74
+ ):
75
+ super().__init__(parent, vin, key)
76
+ self.ble_name = "S" + hashlib.sha1(vin.encode('utf-8')).hexdigest()[:16] + "C"
77
+ self._futures = {}
78
+
79
+ async def discover(self, scanner: BleakScanner = BleakScanner()) -> BleakClient:
80
+ """Find the Tesla BLE device."""
81
+
82
+ device = await scanner.find_device_by_name(self.ble_name)
83
+ if not device:
84
+ raise ValueError(f"Device {self.ble_name} not found")
85
+ self._device = device
86
+ self.client = BleakClient(self._device, services=[SERVICE_UUID])
87
+ LOGGER.debug(f"Discovered device {self._device.name} {self._device.address}")
88
+ return self.client
89
+
90
+ def create_client(self, mac:str):
91
+ """Create a client with a MAC."""
92
+ self.client = BleakClient(mac, services=[SERVICE_UUID])
93
+ return self.client
94
+
95
+ async def connect(self, mac:str | None = None) -> None:
96
+ """Connect to the Tesla BLE device."""
97
+ if mac is not None:
98
+ self.create_client(mac)
99
+ await self.client.connect()
100
+ await self.client.start_notify(READ_UUID, self._on_notify)
101
+
102
+ async def disconnect(self) -> bool:
103
+ """Disconnect from the Tesla BLE device."""
104
+ return await self.client.disconnect()
105
+
106
+ async def __aenter__(self) -> VehicleBluetooth:
107
+ """Enter the async context."""
108
+ await self.connect()
109
+ return self
110
+
111
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
112
+ """Exit the async context."""
113
+ await self.disconnect()
114
+
115
+ def _on_notify(self,sender: BleakGATTCharacteristic,data : bytearray):
116
+ """Receive data from the Tesla BLE device."""
117
+ if self._recv_len:
118
+ self._recv += data
119
+ else:
120
+ self._recv_len = int.from_bytes(data[:2], 'big')
121
+ self._recv = data[2:]
122
+ LOGGER.debug(f"Received {len(self._recv)} of {self._recv_len} bytes")
123
+ while len(self._recv) > self._recv_len:
124
+ LOGGER.warn(f"Received more data than expected: {len(self._recv)} > {self._recv_len}")
125
+ self._on_message(bytes(self._recv[:self._recv_len]))
126
+ self._recv_len = int.from_bytes(self._recv[self._recv_len:self._recv_len+2], 'big')
127
+ self._recv = self._recv[self._recv_len+2:]
128
+ continue
129
+ if len(self._recv) == self._recv_len:
130
+ self._on_message(bytes(self._recv))
131
+ self._recv = bytearray()
132
+ self._recv_len = 0
133
+
134
+ def _on_message(self, data:bytes):
135
+ """Receive messages from the Tesla BLE data."""
136
+ try:
137
+ msg = RoutableMessage.FromString(data)
138
+ except DecodeError as e:
139
+ LOGGER.error(f"Error parsing message: {e}")
140
+ return
141
+
142
+ # Update Session
143
+ if(msg.session_info):
144
+ info = SessionInfo.FromString(msg.session_info)
145
+ LOGGER.debug(f"Received session info: {info}")
146
+ self._sessions[msg.from_destination.domain].update(info)
147
+
148
+ if(msg.to_destination.routing_address != self._from_destination):
149
+ # Get the ephemeral key here and save to self._ekey
150
+ return
151
+
152
+ if msg.from_destination.domain == Domain.DOMAIN_VEHICLE_SECURITY:
153
+ submsg = FromVCSECMessage.FromString(msg.protobuf_message_as_bytes)
154
+ print(submsg)
155
+ elif msg.from_destination.domain == Domain.DOMAIN_INFOTAINMENT:
156
+ submsg = Response.FromString(msg.protobuf_message_as_bytes)
157
+ print(submsg)
158
+
159
+ if(self._futures[msg.from_destination.domain]):
160
+ LOGGER.debug(f"Received response for request {msg.request_uuid}")
161
+ self._futures[msg.from_destination.domain].set_result(msg)
162
+ return
163
+
164
+ async def _create_future(self, domain: Domain) -> Future:
165
+ if(not self._sessions[domain].lock.locked):
166
+ raise ValueError("Session is not locked")
167
+ self._futures[domain] = get_running_loop().create_future()
168
+ return self._futures[domain]
169
+
170
+ async def _send(self, msg: RoutableMessage) -> RoutableMessage:
171
+ """Serialize a message and send to the vehicle and wait for a response."""
172
+ domain = msg.to_destination.domain
173
+ async with self._sessions[domain].lock:
174
+ LOGGER.debug(f"Sending message {msg}")
175
+ future = await self._create_future(domain)
176
+ payload = prependLength(msg.SerializeToString())
177
+
178
+ await self.client.write_gatt_char(WRITE_UUID, payload, True)
179
+
180
+ resp = await future
181
+ LOGGER.debug(f"Received message {resp}")
182
+
183
+ if resp.signedMessageStatus.signed_message_fault:
184
+ raise MESSAGE_FAULTS[resp.signedMessageStatus.signed_message_fault]
185
+
186
+ return resp
187
+
188
+ async def pair(self, role: Role = Role.ROLE_OWNER, form: KeyFormFactor = KeyFormFactor.KEY_FORM_FACTOR_CLOUD_KEY):
189
+ """Pair the key."""
190
+
191
+ request = UnsignedMessage(
192
+ WhitelistOperation=WhitelistOperation(
193
+ addKeyToWhitelistAndAddPermissions=PermissionChange(
194
+ key=PublicKey(PublicKeyRaw=self._public_key),
195
+ keyRole=role
196
+ ),
197
+ metadataForKey=KeyMetadata(
198
+ keyFormFactor=form
199
+ )
200
+ )
201
+ )
202
+ msg = RoutableMessage(
203
+ to_destination=Destination(
204
+ domain=Domain.DOMAIN_VEHICLE_SECURITY
205
+ ),
206
+ from_destination=Destination(
207
+ routing_address=self._from_destination
208
+ ),
209
+ protobuf_message_as_bytes=request.SerializeToString(),
210
+ )
211
+ resp = await self._send(msg)
212
+ respMsg = FromVCSECMessage.FromString(resp.protobuf_message_as_bytes)
213
+ print(respMsg)
214
+ if(respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation):
215
+ if(respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation < len(WHITELIST_OPERATION_STATUS)):
216
+ raise WHITELIST_OPERATION_STATUS[respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation]
217
+ else:
218
+ raise ValueError(f"Unknown whitelist operation status: {respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation}")
219
+ return