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