Skip to content

Commit

Permalink
Merge pull request #458 from zabuldon/dev
Browse files Browse the repository at this point in the history
  • Loading branch information
alandtse authored Feb 25, 2024
2 parents 17802a6 + bdb4497 commit fa43f36
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 18 deletions.
1 change: 1 addition & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@
- InTheDaylight14 [Github](https://github.com/InTheDaylight14)
- Bre77 [Github](https://github.com/Bre77)
- craigrouse [Github](https://github.com/craigrouse)
- thierryVT [Github](https://github.com/thierryvt)
10 changes: 10 additions & 0 deletions DEVELOPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,13 @@ If you're looking to add functionality to Home Assistant you will need to do the
2. Build a proper abstraction inheriting from the [vehicle.py](teslajsonpy/vehicle.py). Check out [lock.py](teslajsonpy/lock.py).
3. Add abstraction to the controller [_add_components](https://github.com/zabuldon/teslajsonpy/blob/dev/teslajsonpy/controller.py#L530) so it will be discoverable.
3. Add changes to Home Assistant to access your abstraction and submit a PR per HA guidelines.

## Experimental support for Tesla HTTP Proxy
Tesla has deprecated the Owner API for modern vehicles.
https://developer.tesla.com/docs/fleet-api#2023-10-09-rest-api-vehicle-commands-endpoint-deprecation-warning
To use the HTTP Proxy, you must provide your Client ID and Proxy URL (e.g. https://tesla.example.com). If your proxy uses a self-signed certificate, you may provide the path to that certificate as a config parameter so it will be trusted.
The proxy server requires the command URL to contain the VIN, not the ID. [Reference](https://github.com/teslamotors/vehicle-command). Otherwise you get an error like this:
```
[teslajsonpy.connection] post: https://local-tesla-http-proxy:4430/api/1/vehicles/xxxxxxxxxxxxxxxx/command/auto_conditioning_start {}
[teslajsonpy.connection] 404: {"response":null,"error":"expected 17-character VIN in path (do not user Fleet API ID)","error_description":""}
```
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@

Async python module for Tesla API primarily for enabling Home-Assistant.

**NOTE:** Tesla has no official API; therefore, this library may stop
working at any time without warning.

# Credits

Originally inspired by [this code.](https://github.com/gglockner/teslajson)
Expand Down
29 changes: 19 additions & 10 deletions teslajsonpy/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from teslajsonpy.const import (
API_URL,
CLIENT_ID,
AUTH_DOMAIN,
DRIVING_INTERVAL,
DOMAIN_KEY,
Expand All @@ -49,18 +50,26 @@ def __init__(
authorization_token: Text = None,
expiration: int = 0,
auth_domain: str = AUTH_DOMAIN,
client_id: str = CLIENT_ID,
api_proxy_url: str = None,
) -> None:
"""Initialize connection object."""
self.user_agent: Text = "TeslaApp/4.10.0"
self.client_id: Text = (
"81527cff06843c8634fdc09e8ac0abef" "b46ac849f38fe1e431c2ef2106796384"
)
self.client_secret: Text = (
"c7257eb71a564034f9419ee651c7d0e5f7" "aa6bfbd18bafb5c5c033b093bb2fa3"
)
self.baseurl: Text = DOMAIN_KEY.get(
auth_domain[auth_domain.rfind(".") :], API_URL
)
if client_id == "ownerapi":
self.client_id: Text = (
"81527cff06843c8634fdc09e8ac0abef" "b46ac849f38fe1e431c2ef2106796384"
)
self.client_secret: Text = (
"c7257eb71a564034f9419ee651c7d0e5f7" "aa6bfbd18bafb5c5c033b093bb2fa3"
)
else:
self.client_id = client_id
if api_proxy_url is None:
self.baseurl: Text = DOMAIN_KEY.get(
auth_domain[auth_domain.rfind(".") :], API_URL
)
else:
self.baseurl: Text = api_proxy_url
self.websocket_url: Text = WS_URL
self.api: Text = "/api/1/"
self.expiration: int = expiration
Expand Down Expand Up @@ -563,7 +572,7 @@ async def refresh_access_token(self, refresh_token):
return
_LOGGER.debug("Refreshing access token with refresh_token")
oauth = {
"client_id": "ownerapi",
"client_id": self.client_id,
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"scope": "openid email offline_access",
Expand Down
1 change: 1 addition & 0 deletions teslajsonpy/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
RELEASE_NOTES_URL = "https://teslascope.com/teslapedia/software/"
AUTH_DOMAIN = "https://auth.tesla.com"
API_URL = "https://owner-api.teslamotors.com"
CLIENT_ID = "ownerapi"
API_URL_CN = "https://owner-api.vn.cloud.tesla.cn"
DOMAIN_KEY = {".com": API_URL, ".cn": API_URL_CN}
WS_URL = "wss://streaming.vn.teslamotors.com/streaming"
Expand Down
60 changes: 55 additions & 5 deletions teslajsonpy/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import logging
import pkgutil
import time
import ssl
from json import JSONDecodeError
from typing import Dict, List, Optional, Set, Text

Expand All @@ -32,6 +33,7 @@
UPDATE_INTERVAL,
WAKE_CHECK_INTERVAL,
WAKE_TIMEOUT,
CLIENT_ID,
)
from teslajsonpy.energy import EnergySite, PowerwallSite, SolarPowerwallSite, SolarSite
from teslajsonpy.exceptions import (
Expand Down Expand Up @@ -104,6 +106,9 @@ def __init__(
enable_websocket: bool = False,
polling_policy: Text = None,
auth_domain: str = AUTH_DOMAIN,
api_proxy_url: str = None,
api_proxy_cert: str = None,
client_id: str = CLIENT_ID
) -> None:
"""Initialize controller.
Expand All @@ -124,18 +129,30 @@ def __init__(
session is complete.
'always' - Keep polling the car at all times. Will possibly never allow the car to sleep.
auth_domain (str, optional): The authentication domain. Defaults to const.AUTH_DOMAIN
api_proxy_url (str, optional): HTTPS Proxy for Fleet API commands
api_proxy_cert (str, optional): Custom SSL certificate to use with proxy
client_id (str, optional): Required for modern vehicles using Fleet API
"""
ssl_context = ssl.create_default_context()
if api_proxy_cert:
try:
ssl_context.load_verify_locations(api_proxy_cert)
except (FileNotFoundError, ssl.SSLError):
_LOGGER.warning("Unable to load custom SSL certificate from %s", api_proxy_cert)

self.__connection = Connection(
websession=websession
if websession and isinstance(websession, httpx.AsyncClient)
else httpx.AsyncClient(timeout=60),
else httpx.AsyncClient(timeout=60, verify=ssl_context),
email=email,
password=password,
access_token=access_token,
refresh_token=refresh_token,
expiration=expiration,
auth_domain=auth_domain,
client_id=client_id,
api_proxy_url=api_proxy_url,
)
self._update_interval: int = update_interval
self._update_interval_vin = {}
Expand Down Expand Up @@ -460,6 +477,7 @@ async def wake_up(self, car_id) -> bool:
)
state = result.get("response", {}).get("state")
self.set_car_online(
vin=car_vin,
car_id=car_id,
online_status=state == "online",
)
Expand All @@ -468,6 +486,7 @@ async def wake_up(self, car_id) -> bool:
response = await self.get_vehicle_summary(vin=car_vin)
state = response.get("state")
self.set_car_online(
vin=car_vin,
car_id=car_id,
online_status=state == "online",
)
Expand All @@ -478,7 +497,10 @@ async def wake_up(self, car_id) -> bool:
time.time() - wake_start_time,
state,
)
return self.is_car_online(vin=car_vin)
return self.is_car_online(
vin=car_vin,
car_id=car_id,
)

def _calculate_next_interval(self, vin: Text) -> int:
cur_time = round(time.time())
Expand Down Expand Up @@ -1223,6 +1245,26 @@ def _process_websocket_disconnect(self, data):
vin = self.__vehicle_id_vin_map[vehicle_id]
_LOGGER.debug("Disconnected %s from websocket", vin[-5:])

def _get_vehicle_ids_for_api(self, path_vars):
vehicle_id = path_vars.get("vehicle_id")
if not vehicle_id:
return None, None

vehicle_id = str(vehicle_id)
if vehicle_id in self.__id_vin_map:
car_id = vehicle_id
car_vin = self.__id_vin_map.get(vehicle_id)
return car_id, car_vin

if vehicle_id in self.__vin_id_map:
car_id = self.__vin_id_map.get(vehicle_id)
car_vin = vehicle_id
return car_id, car_vin

_LOGGER.error("Could not determine correct vehicle ID for API communication: '%s'", vehicle_id)
return None, None


async def api(
self,
name: str,
Expand Down Expand Up @@ -1261,6 +1303,14 @@ async def api(
"""
path_vars = path_vars or {}
# use of car_id was deprecated on the new API but VIN can be used on both new and old so always use vin
car_id, car_vin = self._get_vehicle_ids_for_api(path_vars)
if path_vars.get("vehicle_id"):
if car_vin:
path_vars["vehicle_id"] = car_vin
else:
_LOGGER.warning("WARNING: could not set vehicle_id to car_vin, will attempt to send without overriding but this might cause issues!")

# Load API endpoints once
if not self.endpoints:
try:
Expand Down Expand Up @@ -1288,13 +1338,13 @@ async def api(

# Old @wake_up decorator condensed here
if wake_if_asleep:
car_id = path_vars.get("vehicle_id")
if not car_id:
if not path_vars.get("vehicle_id"):
raise ValueError(
"wake_if_asleep only supported on endpoints with 'vehicle_id' path variable"
)

# If we already know the car is asleep, go ahead and wake it
if not self.is_car_online(car_id=car_id):
if not self.is_car_online(car_id=car_id, vin=car_vin):
await self.wake_up(car_id=car_id)
return await self.__post_with_retries(
"", method=method, data=kwargs, url=uri
Expand Down

0 comments on commit fa43f36

Please sign in to comment.