Skip to content

Commit

Permalink
Merge pull request #31 from alandtse/dev
Browse files Browse the repository at this point in the history
2021-09-10
  • Loading branch information
alandtse authored Sep 11, 2021
2 parents f65d0ce + b09825c commit ff41fb2
Show file tree
Hide file tree
Showing 13 changed files with 319 additions and 855 deletions.
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,11 @@ repos:
rev: 5.7.0
hooks:
- id: isort
- repo: local
hooks:
- id: pytest-check
name: pytest-check
entry: pytest
language: system
pass_filenames: false
always_run: true
4 changes: 2 additions & 2 deletions .prospector.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ ignore-paths:
- docs
- tests
autodetect: true
max-line-length: 88
max-line-length: 108

bandit:
run: false
Expand All @@ -37,7 +37,7 @@ pep8:
enable:
- W601
options:
max-line-length: 79
max-line-length: 108

pep257:
disable:
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@
[![Discord][discord-shield]][discord]
[![Community Forum][forum-shield]][forum]

A fork of the [official Tesla integration](https://www.home-assistant.io/integrations/tesla/) in Home Assistant to use an oauth proxy for logins.
A fork of the [official Tesla integration](https://www.home-assistant.io/integrations/tesla/) in Home Assistant.

This fork uses an oauth proxy instead of screen scraping which was [rejected by HA](https://github.com/home-assistant/core/pull/46558#issuecomment-822858608). The oauth proxy sits as a middleman between Home Assistant and Tesla to intercept login credentials such as your account and password. Due to the way the HTTP server works in Home Assistant, the auth endpoint cannot be turned off although we protect access by requiring knowledge of a ongoing config flow id. However, for maximum security, restart Home Assistant to completely disable the proxy server.

To the extent the official component adds features unrelated to the login, we will attempt to keep up to date. Users are welcome to port any fixes in this custom integration into HA. Please note that this component will not have the same quality or support as the official component. Do not report issues to Home Assistant.
This is the successor to the core app which was removed due to Tesla login issues. Do not report issues to Home Assistant.

To use the component, you will need an application to generate a Tesla refresh token:
- [Tesla Tokens](https://play.google.com/store/apps/details?id=net.leveugle.teslatokens)
- [Auth App for Tesla](https://apps.apple.com/us/app/auth-app-for-tesla/id1552058613)
## Installation

1. Use HACS after adding this `https://github.com/alandtse/tesla` as a custom repository. Skip to 7.
Expand All @@ -29,7 +30,7 @@ To the extent the official component adds features unrelated to the login, we wi
5. Download _all_ the files from the `custom_components/tesla_custom/` directory (folder) in this repository.
6. Place the files you downloaded in the new directory (folder) you created.
7. Restart Home Assistant.
8. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Tesla Custom Integration". If you are replacing core, remove the core integration before installing.
8. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Tesla Custom Integration".

<!---->

Expand Down
105 changes: 69 additions & 36 deletions custom_components/tesla_custom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,24 @@
import logging

import async_timeout
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
CONF_TOKEN,
CONF_USERNAME,
EVENT_HOMEASSISTANT_CLOSE,
HTTP_UNAUTHORIZED,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.httpx_client import SERVER_SOFTWARE, USER_AGENT
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
import httpx
from teslajsonpy import Controller as TeslaAPI
from teslajsonpy.exceptions import IncompleteCredentials, TeslaException
import voluptuous as vol

from .config_flow import CannotConnect, InvalidAuth, validate_input
from .const import (
Expand All @@ -33,6 +38,21 @@

_LOGGER = logging.getLogger(__name__)

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_TOKEN): cv.string,
vol.Optional(
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL)),
}
)
},
extra=vol.ALLOW_EXTRA,
)


@callback
def _async_save_tokens(hass, config_entry, access_token, refresh_token, expiration):
Expand All @@ -50,7 +70,11 @@ def _async_save_tokens(hass, config_entry, access_token, refresh_token, expirati
@callback
def _async_configured_emails(hass):
"""Return a set of configured Tesla emails."""
return {entry.title for entry in hass.config_entries.async_entries(DOMAIN)}
return {
entry.data[CONF_USERNAME]
for entry in hass.config_entries.async_entries(DOMAIN)
if CONF_USERNAME in entry.data
}


async def async_setup(hass, base_config):
Expand All @@ -71,7 +95,7 @@ def _update_entry(email, data=None, options=None):
if not config:
return True
email = config[CONF_USERNAME]
password = config[CONF_PASSWORD]
token = config[CONF_TOKEN]
scan_interval = config[CONF_SCAN_INTERVAL]
if email in _async_configured_emails(hass):
try:
Expand All @@ -81,6 +105,7 @@ def _update_entry(email, data=None, options=None):
_update_entry(
email,
data={
CONF_USERNAME: email,
CONF_ACCESS_TOKEN: info[CONF_ACCESS_TOKEN],
CONF_TOKEN: info[CONF_TOKEN],
CONF_EXPIRATION: info[CONF_EXPIRATION],
Expand All @@ -92,7 +117,7 @@ def _update_entry(email, data=None, options=None):
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={CONF_USERNAME: email, CONF_PASSWORD: password},
data={CONF_USERNAME: email, CONF_TOKEN: token},
)
)
hass.data.setdefault(DOMAIN, {})
Expand All @@ -101,10 +126,12 @@ def _update_entry(email, data=None, options=None):


async def async_setup_entry(hass, config_entry):
# pylint: disable=too-many-locals
"""Set up Tesla as config entry."""
# pylint: disable=too-many-locals
hass.data.setdefault(DOMAIN, {})
config = config_entry.data
# Because users can have multiple accounts, we always create a new session so they have separate cookies
async_client = httpx.AsyncClient(headers={USER_AGENT: SERVER_SOFTWARE}, timeout=60)
email = config_entry.title
if email in hass.data[DOMAIN] and CONF_SCAN_INTERVAL in hass.data[DOMAIN][email]:
scan_interval = hass.data[DOMAIN][email][CONF_SCAN_INTERVAL]
Expand All @@ -114,9 +141,8 @@ async def async_setup_entry(hass, config_entry):
hass.data[DOMAIN].pop(email)
try:
controller = TeslaAPI(
websession=None,
async_client,
email=config.get(CONF_USERNAME),
password=config.get(CONF_PASSWORD),
refresh_token=config[CONF_TOKEN],
access_token=config[CONF_ACCESS_TOKEN],
expiration=config.get(CONF_EXPIRATION, 0),
Expand All @@ -132,14 +158,40 @@ async def async_setup_entry(hass, config_entry):
refresh_token = result["refresh_token"]
access_token = result["access_token"]
expiration = result["expiration"]
except IncompleteCredentials:
_async_start_reauth(hass, config_entry)
return False
except IncompleteCredentials as ex:
await async_client.aclose()
raise ConfigEntryAuthFailed from ex
except httpx.ConnectTimeout as ex:
await async_client.aclose()
raise ConfigEntryNotReady from ex
except TeslaException as ex:
await async_client.aclose()
if ex.code == HTTP_UNAUTHORIZED:
_async_start_reauth(hass, config_entry)
raise ConfigEntryAuthFailed from ex
if ex.message in [
"VEHICLE_UNAVAILABLE",
"TOO_MANY_REQUESTS",
"SERVICE_MAINTENANCE",
"UPSTREAM_TIMEOUT",
]:
raise ConfigEntryNotReady(
f"Temporarily unable to communicate with Tesla API: {ex.message}"
) from ex
_LOGGER.error("Unable to communicate with Tesla API: %s", ex.message)
return False

async def _async_close_client(*_):
await async_client.aclose()

@callback
def _async_create_close_task():
asyncio.create_task(_async_close_client())

config_entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_client)
)
config_entry.async_on_unload(_async_create_close_task)

_async_save_tokens(hass, config_entry, access_token, refresh_token, expiration)
coordinator = TeslaDataUpdateCoordinator(
hass, config_entry=config_entry, controller=controller
Expand All @@ -162,23 +214,15 @@ async def async_setup_entry(hass, config_entry):
for device in all_devices:
entry_data["devices"][device.hass_type].append(device)

for platform in PLATFORMS:
_LOGGER.debug("Loading %s", platform)
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, platform)
)
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)

return True


async def async_unload_entry(hass, config_entry) -> bool:
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(config_entry, platform)
for platform in PLATFORMS
]
)
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)
await hass.data[DOMAIN].get(config_entry.entry_id)[
"coordinator"
Expand All @@ -193,17 +237,6 @@ async def async_unload_entry(hass, config_entry) -> bool:
return False


def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "reauth"},
data=entry.data,
)
)
_LOGGER.error("Credentials are no longer valid. Please reauthenticate")


async def update_listener(hass, config_entry):
"""Update when config_entry options update."""
controller = hass.data[DOMAIN][config_entry.entry_id]["coordinator"].controller
Expand Down
Loading

0 comments on commit ff41fb2

Please sign in to comment.