tesla-fleet-api 1.0.2__tar.gz → 1.0.4__tar.gz
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.
- {tesla_fleet_api-1.0.2/tesla_fleet_api.egg-info → tesla_fleet_api-1.0.4}/PKG-INFO +5 -3
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/README.md +3 -2
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/setup.py +2 -2
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/const.py +15 -1
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/exceptions.py +23 -3
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/bluetooth.py +13 -2
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/partner.py +1 -1
- tesla_fleet_api-1.0.4/tesla_fleet_api/tesla/vehicle/bluetooth.py +397 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/commands.py +127 -47
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/signed.py +8 -14
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/vehicles.py +1 -1
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4/tesla_fleet_api.egg-info}/PKG-INFO +5 -3
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api.egg-info/requires.txt +1 -0
- tesla_fleet_api-1.0.2/tesla_fleet_api/tesla/vehicle/bluetooth.py +0 -226
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/LICENSE +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/pyproject.toml +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/setup.cfg +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/__init__.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/ratecalculator.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/__init__.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/charging.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/energysite.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/fleet.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/oauth.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/tesla.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/user.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/__init__.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/fleet.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/__init__.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/__init__.pyi +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/car_server_pb2.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/car_server_pb2.pyi +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/common_pb2.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/common_pb2.pyi +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/errors_pb2.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/errors_pb2.pyi +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/keys_pb2.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/keys_pb2.pyi +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/managed_charging_pb2.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/managed_charging_pb2.pyi +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/signatures_pb2.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/signatures_pb2.pyi +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/universal_message_pb2.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/universal_message_pb2.pyi +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/vcsec_pb2.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/vcsec_pb2.pyi +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/vehicle_pb2.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/proto/vehicle_pb2.pyi +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tesla/vehicle/vehicle.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/teslemetry/__init__.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/teslemetry/teslemetry.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/teslemetry/vehicle.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tessie/__init__.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tessie/tessie.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api/tessie/vehicle.py +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api.egg-info/SOURCES.txt +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api.egg-info/dependency_links.txt +0 -0
- {tesla_fleet_api-1.0.2 → tesla_fleet_api-1.0.4}/tesla_fleet_api.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: tesla_fleet_api
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.4
|
4
4
|
Summary: Tesla Fleet API library for Python
|
5
5
|
Home-page: https://github.com/Teslemetry/python-tesla-fleet-api
|
6
6
|
Author: Brett Adams
|
@@ -18,6 +18,7 @@ Requires-Dist: aiolimiter
|
|
18
18
|
Requires-Dist: cryptography
|
19
19
|
Requires-Dist: protobuf
|
20
20
|
Requires-Dist: bleak
|
21
|
+
Requires-Dist: bleak-retry-connector
|
21
22
|
Dynamic: author
|
22
23
|
Dynamic: author-email
|
23
24
|
Dynamic: classifier
|
@@ -29,10 +30,11 @@ Dynamic: requires-python
|
|
29
30
|
Dynamic: summary
|
30
31
|
|
31
32
|
# Tesla Fleet Api
|
32
|
-
Python library for Tesla Fleet API and Teslemetry.
|
33
|
+
Python library for Tesla Fleet API and Tesla Command Protocol, including signed commands and encrypted local Bluetooth (BLE). Also provides interfaces for Teslemetry and Tessie.
|
33
34
|
|
34
|
-
Based on [Tesla Developer documentation](https://developer.tesla.com/docs/fleet-api).
|
35
|
+
Based on [Tesla Developer documentation](https://developer.tesla.com/docs/fleet-api) and [Tesla Command Protocol](https://github.com/teslamotors/vehicle-command/blob/main/pkg/protocol/protocol.md)
|
35
36
|
|
37
|
+
**Documentation is currently outdated for V1.0.X**
|
36
38
|
|
37
39
|
## TeslaFleetApi
|
38
40
|
This is the base class, however can also be used directly if you have a valid user access_token.
|
@@ -1,8 +1,9 @@
|
|
1
1
|
# Tesla Fleet Api
|
2
|
-
Python library for Tesla Fleet API and Teslemetry.
|
2
|
+
Python library for Tesla Fleet API and Tesla Command Protocol, including signed commands and encrypted local Bluetooth (BLE). Also provides interfaces for Teslemetry and Tessie.
|
3
3
|
|
4
|
-
Based on [Tesla Developer documentation](https://developer.tesla.com/docs/fleet-api).
|
4
|
+
Based on [Tesla Developer documentation](https://developer.tesla.com/docs/fleet-api) and [Tesla Command Protocol](https://github.com/teslamotors/vehicle-command/blob/main/pkg/protocol/protocol.md)
|
5
5
|
|
6
|
+
**Documentation is currently outdated for V1.0.X**
|
6
7
|
|
7
8
|
## TeslaFleetApi
|
8
9
|
This is the base class, however can also be used directly if you have a valid user access_token.
|
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
|
|
5
5
|
|
6
6
|
setuptools.setup(
|
7
7
|
name="tesla_fleet_api",
|
8
|
-
version="1.0.
|
8
|
+
version="1.0.4",
|
9
9
|
author="Brett Adams",
|
10
10
|
author_email="hello@teslemetry.com",
|
11
11
|
description="Tesla Fleet API library for Python",
|
@@ -20,5 +20,5 @@ setuptools.setup(
|
|
20
20
|
"Operating System :: OS Independent",
|
21
21
|
],
|
22
22
|
python_requires=">=3.10",
|
23
|
-
install_requires=["aiohttp", "aiofiles", "aiolimiter", "cryptography", "protobuf", "bleak"],
|
23
|
+
install_requires=["aiohttp", "aiofiles", "aiolimiter", "cryptography", "protobuf", "bleak", "bleak-retry-connector"],
|
24
24
|
)
|
@@ -3,7 +3,7 @@
|
|
3
3
|
from enum import Enum
|
4
4
|
import logging
|
5
5
|
|
6
|
-
VERSION = "1.0.
|
6
|
+
VERSION = "1.0.4"
|
7
7
|
LOGGER = logging.getLogger(__package__)
|
8
8
|
SERVERS = {
|
9
9
|
"na": "https://fleet-api.prd.na.vn.cloud.tesla.com",
|
@@ -159,3 +159,17 @@ class TeslaEnergyPeriod(StrEnum):
|
|
159
159
|
MONTH = "month"
|
160
160
|
YEAR = "year"
|
161
161
|
LIFETIME = "lifetime"
|
162
|
+
|
163
|
+
class BluetoothVehicleData(StrEnum):
|
164
|
+
CHARGE_STATE = "GetChargeState"
|
165
|
+
CLIMATE_STATE = "GetClimateState"
|
166
|
+
DRIVE_STATE = "GetDriveState"
|
167
|
+
LOCATION_STATE = "GetLocationState"
|
168
|
+
CLOSURES_STATE = "GetClosuresState"
|
169
|
+
CHARGE_SCHEDULE_STATE = "GetChargeScheduleState"
|
170
|
+
PRECONDITIONING_SCHEDULE_STATE = "GetPreconditioningScheduleState"
|
171
|
+
TIRE_PRESSURE_STATE = "GetTirePressureState"
|
172
|
+
MEDIA_STATE = "GetMediaState"
|
173
|
+
MEDIA_DETAIL_STATE = "GetMediaDetailState"
|
174
|
+
SOFTWARE_UPDATE_STATE = "GetSoftwareUpdateState"
|
175
|
+
PARENTAL_CONTROLS_STATE = "GetParentalControlsState"
|
@@ -656,6 +656,26 @@ class TeslaFleetMessageFaultResponseSizeExceedsMTU(TeslaFleetMessageFault):
|
|
656
656
|
message = "Client's request was received, but response size exceeded MTU"
|
657
657
|
code = 25
|
658
658
|
|
659
|
+
class TeslaFleetMessageFaultRepeatedCounter(TeslaFleetMessageFault):
|
660
|
+
"""The vehicle has seen this counter value before. Reset the counter and try again"""
|
661
|
+
|
662
|
+
message = "The vehicle has seen this counter value before. Reset the counter and try again"
|
663
|
+
code = 26
|
664
|
+
|
665
|
+
|
666
|
+
class TeslaFleetMessageFaultInvalidKeyHandle(TeslaFleetMessageFault):
|
667
|
+
"""The key handle is not valid. The key may have been revoked or expired"""
|
668
|
+
|
669
|
+
message = "The key handle is not valid. The key may have been revoked or expired"
|
670
|
+
code = 27
|
671
|
+
|
672
|
+
|
673
|
+
class TeslaFleetMessageFaultRequiresResponseEncryption(TeslaFleetMessageFault):
|
674
|
+
"""The response requires encryption but encryption was not requested"""
|
675
|
+
|
676
|
+
message = "The response requires encryption but encryption was not requested"
|
677
|
+
code = 28
|
678
|
+
|
659
679
|
|
660
680
|
MESSAGE_FAULTS = [
|
661
681
|
None,
|
@@ -684,9 +704,9 @@ MESSAGE_FAULTS = [
|
|
684
704
|
TeslaFleetMessageFaultCommandRequiresAccountCredentials,
|
685
705
|
TeslaFleetMessageFaultFieldExceedsMTU,
|
686
706
|
TeslaFleetMessageFaultResponseSizeExceedsMTU,
|
687
|
-
|
688
|
-
|
689
|
-
|
707
|
+
TeslaFleetMessageFaultRepeatedCounter,
|
708
|
+
TeslaFleetMessageFaultInvalidKeyHandle,
|
709
|
+
TeslaFleetMessageFaultRequiresResponseEncryption,
|
690
710
|
]
|
691
711
|
|
692
712
|
class SignedMessageInformationFault(TeslaFleetError):
|
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
import hashlib
|
4
4
|
import re
|
5
|
+
from google.protobuf.json_format import MessageToJson, MessageToDict
|
6
|
+
from bleak.backends.device import BLEDevice
|
7
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
5
8
|
|
6
9
|
from tesla_fleet_api.tesla.tesla import Tesla
|
7
10
|
from tesla_fleet_api.tesla.vehicle.bluetooth import VehicleBluetooth
|
@@ -36,8 +39,16 @@ class Vehicles(dict[str, VehicleBluetooth]):
|
|
36
39
|
"""Creates a specific vehicle."""
|
37
40
|
return self.createBluetooth(vin)
|
38
41
|
|
39
|
-
def createBluetooth(self, vin: str) -> VehicleBluetooth:
|
42
|
+
def createBluetooth(self, vin: str, key: ec.EllipticCurvePrivateKey | None = None, device: None | str | BLEDevice = None) -> VehicleBluetooth:
|
40
43
|
"""Creates a specific vehicle."""
|
41
|
-
vehicle = VehicleBluetooth(self._parent, vin)
|
44
|
+
vehicle = VehicleBluetooth(self._parent, vin, key, device)
|
42
45
|
self[vin] = vehicle
|
43
46
|
return vehicle
|
47
|
+
|
48
|
+
def toJson(message) -> str:
|
49
|
+
"""Convert a protobuf message to JSON."""
|
50
|
+
return MessageToJson(message, preserving_proto_field_name=True)
|
51
|
+
|
52
|
+
def toDict(message) -> dict:
|
53
|
+
"""Convert a protobuf message to a dictionary."""
|
54
|
+
return MessageToDict(message, preserving_proto_field_name=True)
|
@@ -0,0 +1,397 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import hashlib
|
4
|
+
import asyncio
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
from google.protobuf.message import DecodeError
|
7
|
+
from bleak_retry_connector import establish_connection, MAX_CONNECT_ATTEMPTS
|
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
|
+
BluetoothVehicleData
|
20
|
+
)
|
21
|
+
from tesla_fleet_api.exceptions import (
|
22
|
+
WHITELIST_OPERATION_STATUS,
|
23
|
+
WhitelistOperationStatus,
|
24
|
+
)
|
25
|
+
|
26
|
+
# Protocol
|
27
|
+
from tesla_fleet_api.tesla.vehicle.proto.car_server_pb2 import (
|
28
|
+
Action,
|
29
|
+
VehicleAction,
|
30
|
+
GetVehicleData,
|
31
|
+
GetChargeState,
|
32
|
+
GetClimateState,
|
33
|
+
GetDriveState,
|
34
|
+
GetLocationState,
|
35
|
+
GetClosuresState,
|
36
|
+
GetChargeScheduleState,
|
37
|
+
GetPreconditioningScheduleState,
|
38
|
+
GetTirePressureState,
|
39
|
+
GetMediaState,
|
40
|
+
GetMediaDetailState,
|
41
|
+
GetSoftwareUpdateState,
|
42
|
+
GetParentalControlsState,
|
43
|
+
)
|
44
|
+
from tesla_fleet_api.tesla.vehicle.proto.universal_message_pb2 import (
|
45
|
+
Destination,
|
46
|
+
Domain,
|
47
|
+
RoutableMessage,
|
48
|
+
)
|
49
|
+
from tesla_fleet_api.tesla.vehicle.proto.vcsec_pb2 import (
|
50
|
+
FromVCSECMessage,
|
51
|
+
InformationRequest,
|
52
|
+
InformationRequestType,
|
53
|
+
KeyFormFactor,
|
54
|
+
KeyMetadata,
|
55
|
+
PermissionChange,
|
56
|
+
PublicKey,
|
57
|
+
RKEAction_E,
|
58
|
+
UnsignedMessage,
|
59
|
+
VehicleStatus,
|
60
|
+
WhitelistOperation,
|
61
|
+
)
|
62
|
+
from tesla_fleet_api.tesla.vehicle.proto.vehicle_pb2 import ChargeScheduleState, ChargeState, ClimateState, ClosuresState, DriveState, LocationState, MediaDetailState, MediaState, ParentalControlsState, PreconditioningScheduleState, SoftwareUpdateState, TirePressureState, VehicleData
|
63
|
+
|
64
|
+
SERVICE_UUID = "00000211-b2d1-43f0-9b88-960cebf8b91e"
|
65
|
+
WRITE_UUID = "00000212-b2d1-43f0-9b88-960cebf8b91e"
|
66
|
+
READ_UUID = "00000213-b2d1-43f0-9b88-960cebf8b91e"
|
67
|
+
VERSION_UUID = "00000214-b2d1-43f0-9b88-960cebf8b91e"
|
68
|
+
|
69
|
+
if TYPE_CHECKING:
|
70
|
+
from tesla_fleet_api.tesla.tesla import Tesla
|
71
|
+
|
72
|
+
def prependLength(message: bytes) -> bytearray:
|
73
|
+
"""Prepend a 2-byte length to the payload."""
|
74
|
+
return bytearray([len(message) >> 8, len(message) & 0xFF]) + message
|
75
|
+
|
76
|
+
class VehicleBluetooth(Commands):
|
77
|
+
"""Class describing the Tesla Fleet API vehicle endpoints and commands for a specific vehicle with command signing."""
|
78
|
+
|
79
|
+
ble_name: str
|
80
|
+
device: BLEDevice
|
81
|
+
client: BleakClient
|
82
|
+
_queues: dict[Domain, asyncio.Queue]
|
83
|
+
_ekey: ec.EllipticCurvePublicKey
|
84
|
+
_recv: bytearray = bytearray()
|
85
|
+
_recv_len: int = 0
|
86
|
+
_auth_method = "aes"
|
87
|
+
|
88
|
+
def __init__(
|
89
|
+
self, parent: Tesla, vin: str, key: ec.EllipticCurvePrivateKey | None = None, device: None | str | BLEDevice = None
|
90
|
+
):
|
91
|
+
super().__init__(parent, vin, key)
|
92
|
+
self.ble_name = "S" + hashlib.sha1(vin.encode('utf-8')).hexdigest()[:16] + "C"
|
93
|
+
self._queues = {
|
94
|
+
Domain.DOMAIN_VEHICLE_SECURITY: asyncio.Queue(),
|
95
|
+
Domain.DOMAIN_INFOTAINMENT: asyncio.Queue(),
|
96
|
+
}
|
97
|
+
if device is not None:
|
98
|
+
self.client = BleakClient(device, services=[SERVICE_UUID])
|
99
|
+
|
100
|
+
async def find_vehicle(self, name: str | None = None, address: str | None = None, scanner: BleakScanner = BleakScanner()) -> BLEDevice:
|
101
|
+
"""Find the Tesla BLE device."""
|
102
|
+
if name is not None:
|
103
|
+
device = await scanner.find_device_by_name(name)
|
104
|
+
elif address is not None:
|
105
|
+
device = await scanner.find_device_by_address(address)
|
106
|
+
else:
|
107
|
+
device = await scanner.find_device_by_name(self.ble_name)
|
108
|
+
if not device:
|
109
|
+
raise ValueError(f"Device {self.ble_name} not found")
|
110
|
+
self.device = device
|
111
|
+
return self.device
|
112
|
+
|
113
|
+
def set_device(self, device: BLEDevice) -> None:
|
114
|
+
self.device = device
|
115
|
+
|
116
|
+
def get_device(self) -> BLEDevice:
|
117
|
+
return self.device
|
118
|
+
|
119
|
+
async def connect(self, device: BLEDevice | None = None, max_attempts: int = MAX_CONNECT_ATTEMPTS) -> None:
|
120
|
+
"""Connect to the Tesla BLE device."""
|
121
|
+
self.client = await establish_connection(BleakClient, self.device, self.vin, max_attempts=max_attempts, ble_device_callback=self.get_device)
|
122
|
+
await self.client.start_notify(READ_UUID, self._on_notify)
|
123
|
+
|
124
|
+
async def disconnect(self) -> bool:
|
125
|
+
"""Disconnect from the Tesla BLE device."""
|
126
|
+
return await self.client.disconnect()
|
127
|
+
|
128
|
+
async def __aenter__(self) -> VehicleBluetooth:
|
129
|
+
"""Enter the async context."""
|
130
|
+
await self.connect()
|
131
|
+
return self
|
132
|
+
|
133
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
134
|
+
"""Exit the async context."""
|
135
|
+
await self.disconnect()
|
136
|
+
|
137
|
+
async def _on_notify(self,sender: BleakGATTCharacteristic,data : bytearray) -> None:
|
138
|
+
"""Receive data from the Tesla BLE device."""
|
139
|
+
if self._recv_len:
|
140
|
+
self._recv += data
|
141
|
+
else:
|
142
|
+
self._recv_len = int.from_bytes(data[:2], 'big')
|
143
|
+
self._recv = data[2:]
|
144
|
+
LOGGER.debug(f"Received {len(self._recv)} of {self._recv_len} bytes")
|
145
|
+
while len(self._recv) > self._recv_len:
|
146
|
+
LOGGER.warn(f"Received more data than expected: {len(self._recv)} > {self._recv_len}")
|
147
|
+
await self._on_message(bytes(self._recv[:self._recv_len]))
|
148
|
+
self._recv_len = int.from_bytes(self._recv[self._recv_len:self._recv_len+2], 'big')
|
149
|
+
self._recv = self._recv[self._recv_len+2:]
|
150
|
+
continue
|
151
|
+
if len(self._recv) == self._recv_len:
|
152
|
+
await self._on_message(bytes(self._recv))
|
153
|
+
self._recv = bytearray()
|
154
|
+
self._recv_len = 0
|
155
|
+
|
156
|
+
async def _on_message(self, data:bytes) -> None:
|
157
|
+
"""Receive messages from the Tesla BLE data."""
|
158
|
+
try:
|
159
|
+
msg = RoutableMessage.FromString(data)
|
160
|
+
except DecodeError as e:
|
161
|
+
LOGGER.error(f"Error parsing message: {e}")
|
162
|
+
self._recv = bytearray()
|
163
|
+
self._recv_len = 0
|
164
|
+
return
|
165
|
+
|
166
|
+
if(msg.to_destination.routing_address != self._from_destination):
|
167
|
+
# Ignore ephemeral key broadcasts
|
168
|
+
return
|
169
|
+
|
170
|
+
LOGGER.info(f"Received response: {msg}")
|
171
|
+
await self._queues[msg.from_destination.domain].put(msg)
|
172
|
+
|
173
|
+
async def _send(self, msg: RoutableMessage, requires: str) -> RoutableMessage:
|
174
|
+
"""Serialize a message and send to the vehicle and wait for a response."""
|
175
|
+
domain = msg.to_destination.domain
|
176
|
+
async with self._sessions[domain].lock:
|
177
|
+
LOGGER.debug(f"Sending message {msg}")
|
178
|
+
|
179
|
+
payload = prependLength(msg.SerializeToString())
|
180
|
+
|
181
|
+
# Empty the queue before sending the message
|
182
|
+
while not self._queues[domain].empty():
|
183
|
+
await self._queues[domain].get()
|
184
|
+
await self.client.write_gatt_char(WRITE_UUID, payload, True)
|
185
|
+
|
186
|
+
# Process the response
|
187
|
+
async with asyncio.timeout(10):
|
188
|
+
while True:
|
189
|
+
resp = await self._queues[domain].get()
|
190
|
+
LOGGER.debug(f"Received message {resp}")
|
191
|
+
|
192
|
+
self.validate_msg(resp)
|
193
|
+
|
194
|
+
if resp.HasField(requires):
|
195
|
+
return resp
|
196
|
+
|
197
|
+
async def pair(self, role: Role = Role.ROLE_OWNER, form: KeyFormFactor = KeyFormFactor.KEY_FORM_FACTOR_CLOUD_KEY):
|
198
|
+
"""Pair the key."""
|
199
|
+
|
200
|
+
request = UnsignedMessage(
|
201
|
+
WhitelistOperation=WhitelistOperation(
|
202
|
+
addKeyToWhitelistAndAddPermissions=PermissionChange(
|
203
|
+
key=PublicKey(PublicKeyRaw=self._public_key),
|
204
|
+
keyRole=role
|
205
|
+
),
|
206
|
+
metadataForKey=KeyMetadata(
|
207
|
+
keyFormFactor=form
|
208
|
+
)
|
209
|
+
)
|
210
|
+
)
|
211
|
+
msg = RoutableMessage(
|
212
|
+
to_destination=Destination(
|
213
|
+
domain=Domain.DOMAIN_VEHICLE_SECURITY
|
214
|
+
),
|
215
|
+
from_destination=Destination(
|
216
|
+
routing_address=self._from_destination
|
217
|
+
),
|
218
|
+
protobuf_message_as_bytes=request.SerializeToString(),
|
219
|
+
)
|
220
|
+
resp = await self._send(msg)
|
221
|
+
respMsg = FromVCSECMessage.FromString(resp.protobuf_message_as_bytes)
|
222
|
+
if(respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation):
|
223
|
+
if(respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation < len(WHITELIST_OPERATION_STATUS)):
|
224
|
+
raise WHITELIST_OPERATION_STATUS[respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation]
|
225
|
+
else:
|
226
|
+
raise WhitelistOperationStatus(f"Unknown whitelist operation failure: {respMsg.commandStatus.whitelistOperationStatus.whitelistOperationInformation}")
|
227
|
+
return
|
228
|
+
|
229
|
+
async def wake_up(self):
|
230
|
+
"""Wake up the vehicle."""
|
231
|
+
return await self._sendVehicleSecurity(
|
232
|
+
UnsignedMessage(RKEAction=RKEAction_E.RKE_ACTION_WAKE_VEHICLE)
|
233
|
+
)
|
234
|
+
|
235
|
+
async def vehicle_data(self, endpoints: list[BluetoothVehicleData]) -> VehicleData:
|
236
|
+
"""Get vehicle data."""
|
237
|
+
return await self._getInfotainment(
|
238
|
+
Action(
|
239
|
+
vehicleAction=VehicleAction(
|
240
|
+
getVehicleData=GetVehicleData(
|
241
|
+
getChargeState = GetChargeState() if BluetoothVehicleData.CHARGE_STATE in endpoints else None,
|
242
|
+
getClimateState = GetClimateState() if BluetoothVehicleData.CLIMATE_STATE in endpoints else None,
|
243
|
+
getDriveState = GetDriveState() if BluetoothVehicleData.DRIVE_STATE in endpoints else None,
|
244
|
+
getLocationState = GetLocationState() if BluetoothVehicleData.LOCATION_STATE in endpoints else None,
|
245
|
+
getClosuresState = GetClosuresState() if BluetoothVehicleData.CLOSURES_STATE in endpoints else None,
|
246
|
+
getChargeScheduleState = GetChargeScheduleState() if BluetoothVehicleData.CHARGE_SCHEDULE_STATE in endpoints else None,
|
247
|
+
getPreconditioningScheduleState = GetPreconditioningScheduleState() if BluetoothVehicleData.PRECONDITIONING_SCHEDULE_STATE in endpoints else None,
|
248
|
+
getTirePressureState = GetTirePressureState() if BluetoothVehicleData.TIRE_PRESSURE_STATE in endpoints else None,
|
249
|
+
getMediaState = GetMediaState() if BluetoothVehicleData.MEDIA_STATE in endpoints else None,
|
250
|
+
getMediaDetailState = GetMediaDetailState() if BluetoothVehicleData.MEDIA_DETAIL_STATE in endpoints else None,
|
251
|
+
getSoftwareUpdateState = GetSoftwareUpdateState() if BluetoothVehicleData.SOFTWARE_UPDATE_STATE in endpoints else None,
|
252
|
+
getParentalControlsState = GetParentalControlsState() if BluetoothVehicleData.PARENTAL_CONTROLS_STATE in endpoints else None,
|
253
|
+
)
|
254
|
+
)
|
255
|
+
)
|
256
|
+
)
|
257
|
+
|
258
|
+
async def charge_state(self) -> ChargeState:
|
259
|
+
return (await self._getInfotainment(
|
260
|
+
Action(
|
261
|
+
vehicleAction=VehicleAction(
|
262
|
+
getVehicleData=GetVehicleData(
|
263
|
+
getChargeState=GetChargeState()
|
264
|
+
)
|
265
|
+
)
|
266
|
+
)
|
267
|
+
)).charge_state
|
268
|
+
|
269
|
+
async def climate_state(self) -> ClimateState:
|
270
|
+
return (await self._getInfotainment(
|
271
|
+
Action(
|
272
|
+
vehicleAction=VehicleAction(
|
273
|
+
getVehicleData=GetVehicleData(
|
274
|
+
getClimateState=GetClimateState()
|
275
|
+
)
|
276
|
+
)
|
277
|
+
)
|
278
|
+
)).climate_state
|
279
|
+
|
280
|
+
async def drive_state(self) -> DriveState:
|
281
|
+
return (await self._getInfotainment(
|
282
|
+
Action(
|
283
|
+
vehicleAction=VehicleAction(
|
284
|
+
getVehicleData=GetVehicleData(
|
285
|
+
getDriveState=GetDriveState()
|
286
|
+
)
|
287
|
+
)
|
288
|
+
)
|
289
|
+
)).drive_state
|
290
|
+
|
291
|
+
async def location_state(self) -> LocationState:
|
292
|
+
return (await self._getInfotainment(
|
293
|
+
Action(
|
294
|
+
vehicleAction=VehicleAction(
|
295
|
+
getVehicleData=GetVehicleData(
|
296
|
+
getLocationState=GetLocationState()
|
297
|
+
)
|
298
|
+
)
|
299
|
+
)
|
300
|
+
)).location_state
|
301
|
+
|
302
|
+
async def closures_state(self) -> ClosuresState:
|
303
|
+
return (await self._getInfotainment(
|
304
|
+
Action(
|
305
|
+
vehicleAction=VehicleAction(
|
306
|
+
getVehicleData=GetVehicleData(
|
307
|
+
getClosuresState=GetClosuresState()
|
308
|
+
)
|
309
|
+
)
|
310
|
+
)
|
311
|
+
)).closures_state
|
312
|
+
|
313
|
+
async def charge_schedule_state(self) -> ChargeScheduleState:
|
314
|
+
return (await self._getInfotainment(
|
315
|
+
Action(
|
316
|
+
vehicleAction=VehicleAction(
|
317
|
+
getVehicleData=GetVehicleData(
|
318
|
+
getChargeScheduleState=GetChargeScheduleState()
|
319
|
+
)
|
320
|
+
)
|
321
|
+
)
|
322
|
+
)).charge_schedule_state
|
323
|
+
|
324
|
+
async def preconditioning_schedule_state(self) -> PreconditioningScheduleState:
|
325
|
+
return (await self._getInfotainment(
|
326
|
+
Action(
|
327
|
+
vehicleAction=VehicleAction(
|
328
|
+
getVehicleData=GetVehicleData(
|
329
|
+
getPreconditioningScheduleState=GetPreconditioningScheduleState()
|
330
|
+
)
|
331
|
+
)
|
332
|
+
)
|
333
|
+
)).preconditioning_schedule_state
|
334
|
+
|
335
|
+
async def tire_pressure_state(self) -> TirePressureState:
|
336
|
+
return (await self._getInfotainment(
|
337
|
+
Action(
|
338
|
+
vehicleAction=VehicleAction(
|
339
|
+
getVehicleData=GetVehicleData(
|
340
|
+
getTirePressureState=GetTirePressureState()
|
341
|
+
)
|
342
|
+
)
|
343
|
+
)
|
344
|
+
)).tire_pressure_state
|
345
|
+
|
346
|
+
async def media_state(self) -> MediaState:
|
347
|
+
return (await self._getInfotainment(
|
348
|
+
Action(
|
349
|
+
vehicleAction=VehicleAction(
|
350
|
+
getVehicleData=GetVehicleData(
|
351
|
+
getMediaState=GetMediaState()
|
352
|
+
)
|
353
|
+
)
|
354
|
+
)
|
355
|
+
)).media_state
|
356
|
+
|
357
|
+
async def media_detail_state(self) -> MediaDetailState:
|
358
|
+
return (await self._getInfotainment(
|
359
|
+
Action(
|
360
|
+
vehicleAction=VehicleAction(
|
361
|
+
getVehicleData=GetVehicleData(
|
362
|
+
getMediaDetailState=GetMediaDetailState()
|
363
|
+
)
|
364
|
+
)
|
365
|
+
)
|
366
|
+
)).media_detail_state
|
367
|
+
|
368
|
+
async def software_update_state(self) -> SoftwareUpdateState:
|
369
|
+
return (await self._getInfotainment(
|
370
|
+
Action(
|
371
|
+
vehicleAction=VehicleAction(
|
372
|
+
getVehicleData=GetVehicleData(
|
373
|
+
getSoftwareUpdateState=GetSoftwareUpdateState()
|
374
|
+
)
|
375
|
+
)
|
376
|
+
)
|
377
|
+
)).software_update_state
|
378
|
+
|
379
|
+
async def parental_controls_state(self) -> ParentalControlsState:
|
380
|
+
return (await self._getInfotainment(
|
381
|
+
Action(
|
382
|
+
vehicleAction=VehicleAction(
|
383
|
+
getVehicleData=GetVehicleData(
|
384
|
+
getParentalControlsState=GetParentalControlsState()
|
385
|
+
)
|
386
|
+
)
|
387
|
+
)
|
388
|
+
)).parental_controls_state
|
389
|
+
|
390
|
+
async def vehicle_state(self) -> VehicleStatus:
|
391
|
+
return await self._getVehicleSecurity(
|
392
|
+
UnsignedMessage(
|
393
|
+
InformationRequest=InformationRequest(
|
394
|
+
informationRequestType=InformationRequestType.INFORMATION_REQUEST_TYPE_GET_STATUS
|
395
|
+
)
|
396
|
+
)
|
397
|
+
)
|