tesla-fleet-api 1.0.0__py3-none-any.whl → 1.0.2__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 (49) hide show
  1. tesla_fleet_api/__init__.py +5 -7
  2. tesla_fleet_api/const.py +1 -1
  3. tesla_fleet_api/exceptions.py +16 -2
  4. tesla_fleet_api/tesla/__init__.py +3 -3
  5. tesla_fleet_api/tesla/bluetooth.py +13 -3
  6. tesla_fleet_api/tesla/charging.py +1 -1
  7. tesla_fleet_api/tesla/energysite.py +2 -2
  8. tesla_fleet_api/tesla/fleet.py +8 -8
  9. tesla_fleet_api/tesla/oauth.py +2 -2
  10. tesla_fleet_api/tesla/user.py +1 -1
  11. tesla_fleet_api/tesla/vehicle/__init__.py +13 -0
  12. tesla_fleet_api/tesla/vehicle/bluetooth.py +226 -0
  13. tesla_fleet_api/tesla/vehicle/commands.py +1284 -0
  14. tesla_fleet_api/tesla/vehicle/fleet.py +847 -0
  15. tesla_fleet_api/tesla/vehicle/proto/__init__.py +0 -0
  16. tesla_fleet_api/tesla/vehicle/proto/__init__.pyi +9 -0
  17. tesla_fleet_api/tesla/vehicle/proto/car_server_pb2.py +175 -0
  18. tesla_fleet_api/tesla/vehicle/proto/car_server_pb2.pyi +904 -0
  19. tesla_fleet_api/tesla/vehicle/proto/common_pb2.py +33 -0
  20. tesla_fleet_api/tesla/vehicle/proto/common_pb2.pyi +130 -0
  21. tesla_fleet_api/tesla/vehicle/proto/errors_pb2.py +17 -0
  22. tesla_fleet_api/tesla/vehicle/proto/errors_pb2.pyi +32 -0
  23. tesla_fleet_api/tesla/vehicle/proto/keys_pb2.py +15 -0
  24. tesla_fleet_api/tesla/vehicle/proto/keys_pb2.pyi +21 -0
  25. tesla_fleet_api/tesla/vehicle/proto/managed_charging_pb2.py +15 -0
  26. tesla_fleet_api/tesla/vehicle/proto/managed_charging_pb2.pyi +17 -0
  27. tesla_fleet_api/tesla/vehicle/proto/signatures_pb2.py +35 -0
  28. tesla_fleet_api/tesla/vehicle/proto/signatures_pb2.pyi +152 -0
  29. tesla_fleet_api/tesla/vehicle/proto/universal_message_pb2.py +30 -0
  30. tesla_fleet_api/tesla/vehicle/proto/universal_message_pb2.pyi +148 -0
  31. tesla_fleet_api/tesla/vehicle/proto/vcsec_pb2.py +79 -0
  32. tesla_fleet_api/tesla/vehicle/proto/vcsec_pb2.pyi +482 -0
  33. tesla_fleet_api/tesla/vehicle/proto/vehicle_pb2.py +125 -0
  34. tesla_fleet_api/tesla/vehicle/proto/vehicle_pb2.pyi +1183 -0
  35. tesla_fleet_api/tesla/vehicle/signed.py +55 -0
  36. tesla_fleet_api/tesla/vehicle/vehicle.py +19 -0
  37. tesla_fleet_api/tesla/vehicle/vehicles.py +46 -0
  38. tesla_fleet_api/teslemetry/__init__.py +1 -1
  39. tesla_fleet_api/teslemetry/teslemetry.py +6 -6
  40. tesla_fleet_api/teslemetry/vehicle.py +5 -7
  41. tesla_fleet_api/tessie/__init__.py +1 -1
  42. tesla_fleet_api/tessie/tessie.py +6 -6
  43. tesla_fleet_api/tessie/vehicle.py +3 -9
  44. {tesla_fleet_api-1.0.0.dist-info → tesla_fleet_api-1.0.2.dist-info}/METADATA +1 -1
  45. tesla_fleet_api-1.0.2.dist-info/RECORD +51 -0
  46. tesla_fleet_api-1.0.0.dist-info/RECORD +0 -24
  47. {tesla_fleet_api-1.0.0.dist-info → tesla_fleet_api-1.0.2.dist-info}/LICENSE +0 -0
  48. {tesla_fleet_api-1.0.0.dist-info → tesla_fleet_api-1.0.2.dist-info}/WHEEL +0 -0
  49. {tesla_fleet_api-1.0.0.dist-info → tesla_fleet_api-1.0.2.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,13 @@
1
- from .tesla.fleet import TeslaFleetApi
2
- from .tesla.bluetooth import TeslaBluetooth
3
- from .tesla.oauth import TeslaFleetOAuth
4
- from .tesla.opensource import TeslaFleetOpenSource
5
- from .teslemetry.teslemetry import Teslemetry
6
- from .tessie.tessie import Tessie
1
+ from tesla_fleet_api.tesla.fleet import TeslaFleetApi
2
+ from tesla_fleet_api.tesla.bluetooth import TeslaBluetooth
3
+ from tesla_fleet_api.tesla.oauth import TeslaFleetOAuth
4
+ from tesla_fleet_api.teslemetry.teslemetry import Teslemetry
5
+ from tesla_fleet_api.tessie.tessie import Tessie
7
6
 
8
7
  __all__ = [
9
8
  "TeslaFleetApi",
10
9
  "TeslaBluetooth",
11
10
  "TeslaFleetOAuth",
12
- "TeslaFleetOpenSource",
13
11
  "Teslemetry",
14
12
  "Tessie",
15
13
  ]
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.2"
7
7
  LOGGER = logging.getLogger(__package__)
8
8
  SERVERS = {
9
9
  "na": "https://fleet-api.prd.na.vn.cloud.tesla.com",
@@ -1,5 +1,5 @@
1
1
  import aiohttp
2
- from .const import LOGGER
2
+ from tesla_fleet_api.const import LOGGER
3
3
 
4
4
 
5
5
  class TeslaFleetError(BaseException):
@@ -856,6 +856,9 @@ SIGNED_MESSAGE_INFORMATION_FAULTS = [
856
856
  class WhitelistOperationStatus(TeslaFleetError):
857
857
  message = "Whitelist operation failed"
858
858
 
859
+ def __init__(self, message):
860
+ self.message = message
861
+
859
862
  class WhitelistOperationUndocumentedError(WhitelistOperationStatus):
860
863
  message = "Undocumented whitelist operation error"
861
864
  code = 1
@@ -944,6 +947,14 @@ class WhitelistOperationServiceKeyAttemptingToAddServiceTechOutsideServiceMode(W
944
947
  message = "Service key attempting to add service tech outside service mode"
945
948
  code = 22
946
949
 
950
+ # No idea what 23 & 24 are
951
+
952
+ class WhitelistOperationServiceAuthorizationRequestTimedOut(WhitelistOperationStatus):
953
+ # This is observed but not documented
954
+ message = "Authorization request timed out"
955
+ code = 25
956
+
957
+
947
958
  WHITELIST_OPERATION_STATUS = [
948
959
  None,
949
960
  WhitelistOperationUndocumentedError,
@@ -967,7 +978,10 @@ WHITELIST_OPERATION_STATUS = [
967
978
  WhitelistOperationAttemptingToAddKeyWithoutRole,
968
979
  WhitelistOperationAttemptingToAddKeyWithServiceRole,
969
980
  WhitelistOperationNonServiceKeyAttemptingToAddServiceTech,
970
- WhitelistOperationServiceKeyAttemptingToAddServiceTechOutsideServiceMode
981
+ WhitelistOperationServiceKeyAttemptingToAddServiceTechOutsideServiceMode,
982
+ WhitelistOperationStatus,
983
+ WhitelistOperationStatus,
984
+ WhitelistOperationServiceAuthorizationRequestTimedOut
971
985
  ]
972
986
 
973
987
 
@@ -1,8 +1,8 @@
1
1
  """Tesla Fleet API classes."""
2
2
 
3
- from .fleet import TeslaFleetApi
4
- from .bluetooth import TeslaBluetooth
5
- from .oauth import TeslaFleetOAuth
3
+ from tesla_fleet_api.tesla.fleet import TeslaFleetApi
4
+ from tesla_fleet_api.tesla.bluetooth import TeslaBluetooth
5
+ from tesla_fleet_api.tesla.oauth import TeslaFleetOAuth
6
6
 
7
7
  __all__ = [
8
8
  "TeslaFleetApi",
@@ -1,8 +1,10 @@
1
1
  """Bluetooth only interface."""
2
2
 
3
+ import hashlib
3
4
  import re
4
- from .tesla import Tesla
5
- from .vehicle.bluetooth import VehicleBluetooth
5
+
6
+ from tesla_fleet_api.tesla.tesla import Tesla
7
+ from tesla_fleet_api.tesla.vehicle.bluetooth import VehicleBluetooth
6
8
 
7
9
  class TeslaBluetooth(Tesla):
8
10
  """Class describing a Tesla Bluetooth connection."""
@@ -16,7 +18,11 @@ class TeslaBluetooth(Tesla):
16
18
 
17
19
  def valid_name(self, name: str) -> bool:
18
20
  """Check if a BLE device name is a valid Tesla vehicle."""
19
- return bool(re.match("^S[a-f0-9]{16}[A-F]$", name))
21
+ return bool(re.match("^S[a-f0-9]{16}[CDRP]$", name))
22
+
23
+ def get_name(self, vin: str) -> str:
24
+ """Get the name of a vehicle."""
25
+ return "S" + hashlib.sha1(vin.encode('utf-8')).hexdigest()[:16] + "C"
20
26
 
21
27
  class Vehicles(dict[str, VehicleBluetooth]):
22
28
  """Class containing and creating vehicles."""
@@ -26,6 +32,10 @@ class Vehicles(dict[str, VehicleBluetooth]):
26
32
  def __init__(self, parent: TeslaBluetooth):
27
33
  self._parent = parent
28
34
 
35
+ def create(self, vin: str) -> VehicleBluetooth:
36
+ """Creates a specific vehicle."""
37
+ return self.createBluetooth(vin)
38
+
29
39
  def createBluetooth(self, vin: str) -> VehicleBluetooth:
30
40
  """Creates a specific vehicle."""
31
41
  vehicle = VehicleBluetooth(self._parent, vin)
@@ -1,5 +1,5 @@
1
1
  from typing import Any
2
- from ..const import Method
2
+ from tesla_fleet_api.const import Method
3
3
 
4
4
 
5
5
  class Charging:
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
  from typing import Any, TYPE_CHECKING
3
- from ..const import Method, EnergyOperationMode, EnergyExportMode, TeslaEnergyPeriod
3
+ from tesla_fleet_api.const import Method, EnergyOperationMode, EnergyExportMode, TeslaEnergyPeriod
4
4
 
5
5
  if TYPE_CHECKING:
6
- from . import TeslaFleetApi
6
+ from tesla_fleet_api.tesla.fleet import TeslaFleetApi
7
7
 
8
8
  class EnergySite:
9
9
  """Class describing the Tesla Fleet API partner endpoints"""
@@ -4,14 +4,14 @@ from json import dumps
4
4
  from typing import Any, Awaitable
5
5
  import aiohttp
6
6
 
7
- from .tesla import Tesla
8
- from ..exceptions import raise_for_status, InvalidRegion, LibraryError, ResponseError
9
- from ..const import SERVERS, Method, LOGGER, VERSION
10
- from .charging import Charging
11
- from .energysite import EnergySites
12
- from .partner import Partner
13
- from .user import User
14
- from .vehicle.vehicles import Vehicles
7
+ from tesla_fleet_api.tesla.tesla import Tesla
8
+ from tesla_fleet_api.exceptions import raise_for_status, InvalidRegion, LibraryError, ResponseError
9
+ from tesla_fleet_api.const import SERVERS, Method, LOGGER, VERSION
10
+ from tesla_fleet_api.tesla.charging import Charging
11
+ from tesla_fleet_api.tesla.energysite import EnergySites
12
+ from tesla_fleet_api.tesla.partner import Partner
13
+ from tesla_fleet_api.tesla.user import User
14
+ from tesla_fleet_api.tesla.vehicle.vehicles import Vehicles
15
15
 
16
16
 
17
17
  # Based on https://developer.tesla.com/docs/fleet-api
@@ -2,8 +2,8 @@ from typing import Any
2
2
  import aiohttp
3
3
  import time
4
4
 
5
- from . import TeslaFleetApi
6
- from ..const import Scope, SERVERS, Method
5
+ from tesla_fleet_api.tesla import TeslaFleetApi
6
+ from tesla_fleet_api.const import Scope, SERVERS, Method
7
7
 
8
8
 
9
9
  class TeslaFleetOAuth(TeslaFleetApi):
@@ -1,5 +1,5 @@
1
1
  from typing import Any
2
- from ..const import Method
2
+ from tesla_fleet_api.const import Method
3
3
 
4
4
 
5
5
  class User:
@@ -0,0 +1,13 @@
1
+ """Tesla Fleet API classes."""
2
+
3
+ from tesla_fleet_api.tesla.vehicle.vehicles import Vehicles
4
+ from tesla_fleet_api.tesla.vehicle.fleet import VehicleFleet
5
+ from tesla_fleet_api.tesla.vehicle.bluetooth import VehicleBluetooth
6
+ from tesla_fleet_api.tesla.vehicle.signed import VehicleSigned
7
+
8
+ __all__ = [
9
+ "Vehicles",
10
+ "VehicleFleet",
11
+ "VehicleBluetooth",
12
+ "VehicleSigned",
13
+ ]
@@ -0,0 +1,226 @@
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 tesla_fleet_api.tesla.vehicle.commands import Commands
16
+
17
+ from tesla_fleet_api.const import (
18
+ LOGGER,
19
+ )
20
+ from tesla_fleet_api.exceptions import (
21
+ MESSAGE_FAULTS,
22
+ WHITELIST_OPERATION_STATUS,
23
+ WhitelistOperationStatus,
24
+ )
25
+
26
+ # Protocol
27
+ from tesla_fleet_api.tesla.vehicle.proto.car_server_pb2 import (
28
+ Response,
29
+ )
30
+ from tesla_fleet_api.tesla.vehicle.proto.signatures_pb2 import (
31
+ SessionInfo,
32
+ )
33
+ from tesla_fleet_api.tesla.vehicle.proto.universal_message_pb2 import (
34
+ Destination,
35
+ Domain,
36
+ RoutableMessage,
37
+ )
38
+ from tesla_fleet_api.tesla.vehicle.proto.vcsec_pb2 import (
39
+ FromVCSECMessage,
40
+ KeyFormFactor,
41
+ KeyMetadata,
42
+ PermissionChange,
43
+ PublicKey,
44
+ RKEAction_E,
45
+ UnsignedMessage,
46
+ WhitelistOperation,
47
+
48
+ )
49
+
50
+ SERVICE_UUID = "00000211-b2d1-43f0-9b88-960cebf8b91e"
51
+ WRITE_UUID = "00000212-b2d1-43f0-9b88-960cebf8b91e"
52
+ READ_UUID = "00000213-b2d1-43f0-9b88-960cebf8b91e"
53
+ VERSION_UUID = "00000214-b2d1-43f0-9b88-960cebf8b91e"
54
+
55
+ if TYPE_CHECKING:
56
+ from tesla_fleet_api.tesla.tesla import Tesla
57
+
58
+ def prependLength(message: bytes) -> bytearray:
59
+ """Prepend a 2-byte length to the payload."""
60
+ return bytearray([len(message) >> 8, len(message) & 0xFF]) + message
61
+
62
+ class VehicleBluetooth(Commands):
63
+ """Class describing the Tesla Fleet API vehicle endpoints and commands for a specific vehicle with command signing."""
64
+
65
+ ble_name: str
66
+ client: BleakClient
67
+ _device: BLEDevice
68
+ _futures: dict[Domain, Future]
69
+ _ekey: ec.EllipticCurvePublicKey
70
+ _recv: bytearray = bytearray()
71
+ _recv_len: int = 0
72
+ _auth_method = "aes"
73
+
74
+ def __init__(
75
+ self, parent: Tesla, vin: str, key: ec.EllipticCurvePrivateKey | None = None
76
+ ):
77
+ super().__init__(parent, vin, key)
78
+ self.ble_name = "S" + hashlib.sha1(vin.encode('utf-8')).hexdigest()[:16] + "C"
79
+ self._futures = {}
80
+
81
+ async def find_client(self, scanner: BleakScanner = BleakScanner()) -> BleakClient:
82
+ """Find the Tesla BLE device."""
83
+
84
+ device = await scanner.find_device_by_name(self.ble_name)
85
+ if not device:
86
+ raise ValueError(f"Device {self.ble_name} not found")
87
+ self._device = device
88
+ self.client = BleakClient(self._device, services=[SERVICE_UUID])
89
+ LOGGER.debug(f"Discovered device {self._device.name} {self._device.address}")
90
+ return self.client
91
+
92
+ def create_client(self, mac:str) -> BleakClient:
93
+ """Create a client using a MAC address."""
94
+ self.client = BleakClient(mac, services=[SERVICE_UUID])
95
+ return self.client
96
+
97
+ async def connect(self, mac:str | None = None) -> None:
98
+ """Connect to the Tesla BLE device."""
99
+ if mac is not None:
100
+ self.create_client(mac)
101
+ await self.client.connect()
102
+ await self.client.start_notify(READ_UUID, self._on_notify)
103
+
104
+ async def disconnect(self) -> bool:
105
+ """Disconnect from the Tesla BLE device."""
106
+ return await self.client.disconnect()
107
+
108
+ async def __aenter__(self) -> VehicleBluetooth:
109
+ """Enter the async context."""
110
+ await self.connect()
111
+ return self
112
+
113
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
114
+ """Exit the async context."""
115
+ await self.disconnect()
116
+
117
+ def _on_notify(self,sender: BleakGATTCharacteristic,data : bytearray) -> None:
118
+ """Receive data from the Tesla BLE device."""
119
+ if self._recv_len:
120
+ self._recv += data
121
+ else:
122
+ self._recv_len = int.from_bytes(data[:2], 'big')
123
+ self._recv = data[2:]
124
+ LOGGER.debug(f"Received {len(self._recv)} of {self._recv_len} bytes")
125
+ while len(self._recv) > self._recv_len:
126
+ LOGGER.warn(f"Received more data than expected: {len(self._recv)} > {self._recv_len}")
127
+ self._on_message(bytes(self._recv[:self._recv_len]))
128
+ self._recv_len = int.from_bytes(self._recv[self._recv_len:self._recv_len+2], 'big')
129
+ self._recv = self._recv[self._recv_len+2:]
130
+ continue
131
+ if len(self._recv) == self._recv_len:
132
+ self._on_message(bytes(self._recv))
133
+ self._recv = bytearray()
134
+ self._recv_len = 0
135
+
136
+ def _on_message(self, data:bytes) -> None:
137
+ """Receive messages from the Tesla BLE data."""
138
+ try:
139
+ msg = RoutableMessage.FromString(data)
140
+ except DecodeError as e:
141
+ LOGGER.error(f"Error parsing message: {e}")
142
+ return
143
+
144
+ # Update Session
145
+ if(msg.session_info):
146
+ info = SessionInfo.FromString(msg.session_info)
147
+ LOGGER.debug(f"Received session info: {info}")
148
+ self._sessions[msg.from_destination.domain].update(info)
149
+
150
+ if(msg.to_destination.routing_address != self._from_destination):
151
+ # Get the ephemeral key here and save to self._ekey
152
+ return
153
+
154
+ if(self._futures[msg.from_destination.domain]):
155
+ LOGGER.debug(f"Received response for request {msg.request_uuid}")
156
+ self._futures[msg.from_destination.domain].set_result(msg)
157
+ return
158
+
159
+ if msg.from_destination.domain == Domain.DOMAIN_VEHICLE_SECURITY:
160
+ submsg = FromVCSECMessage.FromString(msg.protobuf_message_as_bytes)
161
+ LOGGER.warning(f"Received orphaned VCSEC response: {submsg}")
162
+ elif msg.from_destination.domain == Domain.DOMAIN_INFOTAINMENT:
163
+ submsg = Response.FromString(msg.protobuf_message_as_bytes)
164
+ LOGGER.warning(f"Received orphaned INFOTAINMENT response: {submsg}")
165
+
166
+ async def _create_future(self, domain: Domain) -> Future:
167
+ if(not self._sessions[domain].lock.locked):
168
+ raise ValueError("Session is not locked")
169
+ self._futures[domain] = get_running_loop().create_future()
170
+ return self._futures[domain]
171
+
172
+ async def _send(self, msg: RoutableMessage) -> RoutableMessage:
173
+ """Serialize a message and send to the vehicle and wait for a response."""
174
+ domain = msg.to_destination.domain
175
+ async with self._sessions[domain].lock:
176
+ LOGGER.debug(f"Sending message {msg}")
177
+ future = await self._create_future(domain)
178
+ payload = prependLength(msg.SerializeToString())
179
+
180
+ await self.client.write_gatt_char(WRITE_UUID, payload, True)
181
+
182
+ resp = await future
183
+ LOGGER.debug(f"Received message {resp}")
184
+
185
+ if resp.signedMessageStatus.signed_message_fault:
186
+ raise MESSAGE_FAULTS[resp.signedMessageStatus.signed_message_fault]
187
+
188
+ return resp
189
+
190
+ async def pair(self, role: Role = Role.ROLE_OWNER, form: KeyFormFactor = KeyFormFactor.KEY_FORM_FACTOR_CLOUD_KEY):
191
+ """Pair the key."""
192
+
193
+ request = UnsignedMessage(
194
+ WhitelistOperation=WhitelistOperation(
195
+ addKeyToWhitelistAndAddPermissions=PermissionChange(
196
+ key=PublicKey(PublicKeyRaw=self._public_key),
197
+ keyRole=role
198
+ ),
199
+ metadataForKey=KeyMetadata(
200
+ keyFormFactor=form
201
+ )
202
+ )
203
+ )
204
+ msg = RoutableMessage(
205
+ to_destination=Destination(
206
+ domain=Domain.DOMAIN_VEHICLE_SECURITY
207
+ ),
208
+ from_destination=Destination(
209
+ routing_address=self._from_destination
210
+ ),
211
+ protobuf_message_as_bytes=request.SerializeToString(),
212
+ )
213
+ resp = await self._send(msg)
214
+ respMsg = FromVCSECMessage.FromString(resp.protobuf_message_as_bytes)
215
+ if(respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation):
216
+ if(respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation < len(WHITELIST_OPERATION_STATUS)):
217
+ raise WHITELIST_OPERATION_STATUS[respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation]
218
+ else:
219
+ raise WhitelistOperationStatus(f"Unknown whitelist operation failure: {respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation}")
220
+ return
221
+
222
+ async def wake_up(self):
223
+ """Wake up the vehicle."""
224
+ return await self._sendVehicleSecurity(
225
+ UnsignedMessage(RKEAction=RKEAction_E.RKE_ACTION_WAKE_VEHICLE)
226
+ )