tesla-fleet-api 1.0.0__tar.gz → 1.0.1__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {tesla_fleet_api-1.0.0/tesla_fleet_api.egg-info → tesla_fleet_api-1.0.1}/PKG-INFO +1 -1
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/setup.py +1 -1
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/const.py +1 -1
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/tesla/bluetooth.py +6 -1
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/__init__.py +13 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/bluetooth.py +219 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/commands.py +1286 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/fleet.py +847 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/__init__.py +0 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/__init__.pyi +9 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/car_server_pb2.py +175 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/car_server_pb2.pyi +904 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/common_pb2.py +33 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/common_pb2.pyi +130 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/errors_pb2.py +17 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/errors_pb2.pyi +32 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/keys_pb2.py +15 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/keys_pb2.pyi +21 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/managed_charging_pb2.py +15 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/managed_charging_pb2.pyi +17 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/signatures_pb2.py +35 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/signatures_pb2.pyi +152 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/universal_message_pb2.py +30 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/universal_message_pb2.pyi +148 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/vcsec_pb2.py +79 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/vcsec_pb2.pyi +482 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/vehicle_pb2.py +125 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/proto/vehicle_pb2.pyi +1183 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/signed.py +56 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/vehicle.py +19 -0
- tesla_fleet_api-1.0.1/tesla_fleet_api/tesla/vehicle/vehicles.py +46 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1/tesla_fleet_api.egg-info}/PKG-INFO +1 -1
- tesla_fleet_api-1.0.1/tesla_fleet_api.egg-info/SOURCES.txt +55 -0
- tesla_fleet_api-1.0.0/tesla_fleet_api.egg-info/SOURCES.txt +0 -28
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/LICENSE +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/README.md +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/pyproject.toml +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/setup.cfg +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/__init__.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/exceptions.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/ratecalculator.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/tesla/__init__.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/tesla/charging.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/tesla/energysite.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/tesla/fleet.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/tesla/oauth.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/tesla/partner.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/tesla/tesla.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/tesla/user.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/teslemetry/__init__.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/teslemetry/teslemetry.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/teslemetry/vehicle.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/tessie/__init__.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/tessie/tessie.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api/tessie/vehicle.py +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api.egg-info/dependency_links.txt +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api.egg-info/requires.txt +0 -0
- {tesla_fleet_api-1.0.0 → tesla_fleet_api-1.0.1}/tesla_fleet_api.egg-info/top_level.txt +0 -0
@@ -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}[
|
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
|