Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for HTTP Proxy #454

Merged
merged 2 commits into from
Feb 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1216,6 +1238,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 @@ -1254,6 +1296,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 @@ -1281,13 +1331,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
Loading