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.
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
@@ -2,26 +2,20 @@
2
2
 
3
3
  from json import dumps
4
4
  from typing import Any, Awaitable
5
- from os.path import exists
6
5
  import aiohttp
7
- import aiofiles
8
6
 
9
- # cryptography
10
- from cryptography.hazmat.primitives.asymmetric import ec
11
- from cryptography.hazmat.primitives import serialization
12
- from cryptography.hazmat.backends import default_backend
13
-
14
- from .exceptions import raise_for_status, InvalidRegion, LibraryError, ResponseError
15
- from .const import SERVERS, Method, LOGGER, VERSION
7
+ from .tesla import Tesla
8
+ from ..exceptions import raise_for_status, InvalidRegion, LibraryError, ResponseError
9
+ from ..const import SERVERS, Method, LOGGER, VERSION
16
10
  from .charging import Charging
17
- from .energy import Energy
11
+ from .energysite import EnergySites
18
12
  from .partner import Partner
19
13
  from .user import User
20
- from .vehicle import Vehicle
14
+ from .vehicle.vehicles import Vehicles
21
15
 
22
16
 
23
17
  # Based on https://developer.tesla.com/docs/fleet-api
24
- class TeslaFleetApi:
18
+ class TeslaFleetApi(Tesla):
25
19
  """Class describing the Tesla Fleet API."""
26
20
 
27
21
  access_token: str | None = None
@@ -30,7 +24,6 @@ class TeslaFleetApi:
30
24
  session: aiohttp.ClientSession
31
25
  headers: dict[str, str]
32
26
  refresh_hook: Awaitable | None = None
33
- private_key: ec.EllipticCurvePrivateKey | None = None
34
27
 
35
28
  def __init__(
36
29
  self,
@@ -63,13 +56,13 @@ class TeslaFleetApi:
63
56
  if charging_scope:
64
57
  self.charging = Charging(self)
65
58
  if energy_scope:
66
- self.energy = Energy(self)
59
+ self.energySites = EnergySites(self)
67
60
  if user_scope:
68
61
  self.user = User(self)
69
62
  if partner_scope:
70
63
  self.partner = Partner(self)
71
64
  if vehicle_scope:
72
- self.vehicle = Vehicle(self)
65
+ self.vehicles = Vehicles(self)
73
66
 
74
67
  async def find_server(self) -> str:
75
68
  """Find the server URL for the Tesla Fleet API."""
@@ -162,41 +155,3 @@ class TeslaFleetApi:
162
155
  Method.GET,
163
156
  "api/1/products",
164
157
  )
165
-
166
- async def get_private_key(
167
- self, path: str = "private_key.pem"
168
- ) -> ec.EllipticCurvePrivateKey:
169
- """Get or create the private key."""
170
- if not exists(path):
171
- self.private_key = ec.generate_private_key(
172
- ec.SECP256R1(), default_backend()
173
- )
174
- # save the key
175
- pem = self.private_key.private_bytes(
176
- encoding=serialization.Encoding.PEM,
177
- format=serialization.PrivateFormat.TraditionalOpenSSL,
178
- encryption_algorithm=serialization.NoEncryption(),
179
- )
180
- async with aiofiles.open(path, "wb") as key_file:
181
- await key_file.write(pem)
182
- else:
183
- try:
184
- async with aiofiles.open(path, "rb") as key_file:
185
- key_data = await key_file.read()
186
- value = serialization.load_pem_private_key(
187
- key_data, password=None, backend=default_backend()
188
- )
189
- except FileNotFoundError:
190
- raise FileNotFoundError(f"Private key file not found at {path}")
191
- except PermissionError:
192
- raise PermissionError(f"Permission denied when trying to read {path}")
193
-
194
- if not isinstance(value, ec.EllipticCurvePrivateKey):
195
- raise AssertionError("Loaded key is not an EllipticCurvePrivateKey")
196
- self.private_key = value
197
- return self.private_key
198
-
199
- @property
200
- def has_private_key(self) -> bool:
201
- """Check if the private key has been set."""
202
- return self.private_key is not None
@@ -2,16 +2,15 @@ from typing import Any
2
2
  import aiohttp
3
3
  import time
4
4
 
5
- from tesla_fleet_api.const import Method
6
- from .teslafleetapi import TeslaFleetApi
7
- from .const import Scope, SERVERS
5
+ from . import TeslaFleetApi
6
+ from ..const import Scope, SERVERS, Method
8
7
 
9
8
 
10
9
  class TeslaFleetOAuth(TeslaFleetApi):
11
10
  """Tesla Fleet OAuth API."""
12
11
 
13
12
  expires: int
14
- refresh_token: str
13
+ refresh_token: str | None
15
14
  redirect_uri: str | None
16
15
  _client_secret: str | None
17
16
 
@@ -1,5 +1,5 @@
1
1
  from typing import Any
2
- from .const import Method
2
+ from ..const import Method
3
3
 
4
4
 
5
5
  class Partner:
@@ -0,0 +1,52 @@
1
+ """Tesla Fleet API for Python."""
2
+
3
+ from os.path import exists
4
+ import aiofiles
5
+
6
+ # cryptography
7
+ from cryptography.hazmat.primitives.asymmetric import ec
8
+ from cryptography.hazmat.primitives import serialization
9
+ from cryptography.hazmat.backends import default_backend
10
+
11
+ class Tesla:
12
+ """Base class describing interactions with Tesla products."""
13
+
14
+ private_key: ec.EllipticCurvePrivateKey | None = None
15
+
16
+ async def get_private_key(
17
+ self, path: str = "private_key.pem"
18
+ ) -> ec.EllipticCurvePrivateKey:
19
+ """Get or create the private key."""
20
+ if not exists(path):
21
+ self.private_key = ec.generate_private_key(
22
+ ec.SECP256R1(), default_backend()
23
+ )
24
+ # save the key
25
+ pem = self.private_key.private_bytes(
26
+ encoding=serialization.Encoding.PEM,
27
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
28
+ encryption_algorithm=serialization.NoEncryption(),
29
+ )
30
+ async with aiofiles.open(path, "wb") as key_file:
31
+ await key_file.write(pem)
32
+ else:
33
+ try:
34
+ async with aiofiles.open(path, "rb") as key_file:
35
+ key_data = await key_file.read()
36
+ value = serialization.load_pem_private_key(
37
+ key_data, password=None, backend=default_backend()
38
+ )
39
+ except FileNotFoundError:
40
+ raise FileNotFoundError(f"Private key file not found at {path}")
41
+ except PermissionError:
42
+ raise PermissionError(f"Permission denied when trying to read {path}")
43
+
44
+ if not isinstance(value, ec.EllipticCurvePrivateKey):
45
+ raise AssertionError("Loaded key is not an EllipticCurvePrivateKey")
46
+ self.private_key = value
47
+ return self.private_key
48
+
49
+ @property
50
+ def has_private_key(self) -> bool:
51
+ """Check if the private key has been set."""
52
+ return self.private_key is not None
@@ -1,5 +1,5 @@
1
1
  from typing import Any
2
- from .const import Method
2
+ from ..const import Method
3
3
 
4
4
 
5
5
  class User:
@@ -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