Bump Tesla dependency teslajsonpy to 0.18.3 (#49939)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Alan Tse 2021-05-01 17:04:37 -07:00 committed by GitHub
parent 796f9cad1f
commit 3546ff2da2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 118 additions and 38 deletions

View File

@ -1,9 +1,11 @@
"""Support for Tesla cars.""" """Support for Tesla cars."""
import asyncio
from collections import defaultdict from collections import defaultdict
from datetime import timedelta from datetime import timedelta
import logging import logging
import async_timeout import async_timeout
import httpx
from teslajsonpy import Controller as TeslaAPI from teslajsonpy import Controller as TeslaAPI
from teslajsonpy.exceptions import IncompleteCredentials, TeslaException from teslajsonpy.exceptions import IncompleteCredentials, TeslaException
import voluptuous as vol import voluptuous as vol
@ -17,11 +19,13 @@ from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
CONF_TOKEN, CONF_TOKEN,
CONF_USERNAME, CONF_USERNAME,
EVENT_HOMEASSISTANT_CLOSE,
HTTP_UNAUTHORIZED, HTTP_UNAUTHORIZED,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.httpx_client import SERVER_SOFTWARE, USER_AGENT
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator, DataUpdateCoordinator,
@ -31,6 +35,7 @@ from homeassistant.util import slugify
from .config_flow import CannotConnect, InvalidAuth, validate_input from .config_flow import CannotConnect, InvalidAuth, validate_input
from .const import ( from .const import (
CONF_EXPIRATION,
CONF_WAKE_ON_START, CONF_WAKE_ON_START,
DATA_LISTENER, DATA_LISTENER,
DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
@ -113,6 +118,7 @@ async def async_setup(hass, base_config):
CONF_PASSWORD: password, CONF_PASSWORD: password,
CONF_ACCESS_TOKEN: info[CONF_ACCESS_TOKEN], CONF_ACCESS_TOKEN: info[CONF_ACCESS_TOKEN],
CONF_TOKEN: info[CONF_TOKEN], CONF_TOKEN: info[CONF_TOKEN],
CONF_EXPIRATION: info[CONF_EXPIRATION],
}, },
options={CONF_SCAN_INTERVAL: scan_interval}, options={CONF_SCAN_INTERVAL: scan_interval},
) )
@ -134,7 +140,7 @@ async def async_setup_entry(hass, config_entry):
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
config = config_entry.data config = config_entry.data
# Because users can have multiple accounts, we always create a new session so they have separate cookies # Because users can have multiple accounts, we always create a new session so they have separate cookies
websession = aiohttp_client.async_create_clientsession(hass) async_client = httpx.AsyncClient(headers={USER_AGENT: SERVER_SOFTWARE})
email = config_entry.title email = config_entry.title
if email in hass.data[DOMAIN] and CONF_SCAN_INTERVAL in hass.data[DOMAIN][email]: if email in hass.data[DOMAIN] and CONF_SCAN_INTERVAL in hass.data[DOMAIN][email]:
scan_interval = hass.data[DOMAIN][email][CONF_SCAN_INTERVAL] scan_interval = hass.data[DOMAIN][email][CONF_SCAN_INTERVAL]
@ -144,27 +150,45 @@ async def async_setup_entry(hass, config_entry):
hass.data[DOMAIN].pop(email) hass.data[DOMAIN].pop(email)
try: try:
controller = TeslaAPI( controller = TeslaAPI(
websession, async_client,
email=config.get(CONF_USERNAME), email=config.get(CONF_USERNAME),
password=config.get(CONF_PASSWORD), password=config.get(CONF_PASSWORD),
refresh_token=config[CONF_TOKEN], refresh_token=config[CONF_TOKEN],
access_token=config[CONF_ACCESS_TOKEN], access_token=config[CONF_ACCESS_TOKEN],
expiration=config.get(CONF_EXPIRATION, 0),
update_interval=config_entry.options.get( update_interval=config_entry.options.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
), ),
) )
(refresh_token, access_token) = await controller.connect( result = await controller.connect(
wake_if_asleep=config_entry.options.get( wake_if_asleep=config_entry.options.get(
CONF_WAKE_ON_START, DEFAULT_WAKE_ON_START CONF_WAKE_ON_START, DEFAULT_WAKE_ON_START
) )
) )
refresh_token = result["refresh_token"]
access_token = result["access_token"]
except IncompleteCredentials as ex: except IncompleteCredentials as ex:
await async_client.aclose()
raise ConfigEntryAuthFailed from ex raise ConfigEntryAuthFailed from ex
except TeslaException as ex: except TeslaException as ex:
await async_client.aclose()
if ex.code == HTTP_UNAUTHORIZED: if ex.code == HTTP_UNAUTHORIZED:
raise ConfigEntryAuthFailed from ex raise ConfigEntryAuthFailed from ex
_LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message)
return False 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) _async_save_tokens(hass, config_entry, access_token, refresh_token)
coordinator = TeslaDataUpdateCoordinator( coordinator = TeslaDataUpdateCoordinator(
hass, config_entry=config_entry, controller=controller hass, config_entry=config_entry, controller=controller
@ -240,7 +264,9 @@ class TeslaDataUpdateCoordinator(DataUpdateCoordinator):
async def _async_update_data(self): async def _async_update_data(self):
"""Fetch data from API endpoint.""" """Fetch data from API endpoint."""
if self.controller.is_token_refreshed(): if self.controller.is_token_refreshed():
(refresh_token, access_token) = self.controller.get_tokens() result = self.controller.get_tokens()
refresh_token = result["refresh_token"]
access_token = result["access_token"]
_async_save_tokens( _async_save_tokens(
self.hass, self.config_entry, access_token, refresh_token self.hass, self.config_entry, access_token, refresh_token
) )

View File

@ -1,7 +1,9 @@
"""Tesla Config Flow.""" """Tesla Config Flow."""
import logging import logging
import httpx
from teslajsonpy import Controller as TeslaAPI, TeslaException from teslajsonpy import Controller as TeslaAPI, TeslaException
from teslajsonpy.exceptions import IncompleteCredentials
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, core, exceptions from homeassistant import config_entries, core, exceptions
@ -14,9 +16,11 @@ from homeassistant.const import (
HTTP_UNAUTHORIZED, HTTP_UNAUTHORIZED,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.httpx_client import SERVER_SOFTWARE, USER_AGENT
from .const import ( from .const import (
CONF_EXPIRATION,
CONF_WAKE_ON_START, CONF_WAKE_ON_START,
DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
DEFAULT_WAKE_ON_START, DEFAULT_WAKE_ON_START,
@ -35,6 +39,7 @@ class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self): def __init__(self):
"""Initialize the tesla flow.""" """Initialize the tesla flow."""
self.username = None self.username = None
self.reauth = False
async def async_step_import(self, import_config): async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml.""" """Import a config entry from configuration.yaml."""
@ -46,10 +51,7 @@ class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None: if user_input is not None:
existing_entry = self._async_entry_for_username(user_input[CONF_USERNAME]) existing_entry = self._async_entry_for_username(user_input[CONF_USERNAME])
if ( if existing_entry and not self.reauth:
existing_entry
and existing_entry.data[CONF_PASSWORD] == user_input[CONF_PASSWORD]
):
return self.async_abort(reason="already_configured") return self.async_abort(reason="already_configured")
try: try:
@ -81,6 +83,7 @@ class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_reauth(self, data): async def async_step_reauth(self, data):
"""Handle configuration by re-auth.""" """Handle configuration by re-auth."""
self.username = data[CONF_USERNAME] self.username = data[CONF_USERNAME]
self.reauth = True
return await self.async_step_user() return await self.async_step_user()
@staticmethod @staticmethod
@ -146,26 +149,32 @@ async def validate_input(hass: core.HomeAssistant, data):
""" """
config = {} config = {}
websession = aiohttp_client.async_create_clientsession(hass) async_client = httpx.AsyncClient(headers={USER_AGENT: SERVER_SOFTWARE})
try: try:
controller = TeslaAPI( controller = TeslaAPI(
websession, async_client,
email=data[CONF_USERNAME], email=data[CONF_USERNAME],
password=data[CONF_PASSWORD], password=data[CONF_PASSWORD],
update_interval=DEFAULT_SCAN_INTERVAL, update_interval=DEFAULT_SCAN_INTERVAL,
) )
(config[CONF_TOKEN], config[CONF_ACCESS_TOKEN]) = await controller.connect( result = await controller.connect(test_login=True)
test_login=True config[CONF_TOKEN] = result["refresh_token"]
) config[CONF_ACCESS_TOKEN] = result["access_token"]
config[CONF_EXPIRATION] = result[CONF_EXPIRATION]
config[CONF_USERNAME] = data[CONF_USERNAME] config[CONF_USERNAME] = data[CONF_USERNAME]
config[CONF_PASSWORD] = data[CONF_PASSWORD] config[CONF_PASSWORD] = data[CONF_PASSWORD]
except IncompleteCredentials as ex:
_LOGGER.error("Authentication error: %s %s", ex.message, ex)
raise InvalidAuth() from ex
except TeslaException as ex: except TeslaException as ex:
if ex.code == HTTP_UNAUTHORIZED: if ex.code == HTTP_UNAUTHORIZED:
_LOGGER.error("Invalid credentials: %s", ex) _LOGGER.error("Invalid credentials: %s", ex)
raise InvalidAuth() from ex raise InvalidAuth() from ex
_LOGGER.error("Unable to communicate with Tesla API: %s", ex) _LOGGER.error("Unable to communicate with Tesla API: %s", ex)
raise CannotConnect() from ex raise CannotConnect() from ex
finally:
await async_client.aclose()
_LOGGER.debug("Credentials successfully connected to the Tesla API") _LOGGER.debug("Credentials successfully connected to the Tesla API")
return config return config

View File

@ -1,4 +1,5 @@
"""Const file for Tesla cars.""" """Const file for Tesla cars."""
CONF_EXPIRATION = "expiration"
CONF_WAKE_ON_START = "enable_wake_on_start" CONF_WAKE_ON_START = "enable_wake_on_start"
DOMAIN = "tesla" DOMAIN = "tesla"
DATA_LISTENER = "listener" DATA_LISTENER = "listener"

View File

@ -3,7 +3,7 @@
"name": "Tesla", "name": "Tesla",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tesla", "documentation": "https://www.home-assistant.io/integrations/tesla",
"requirements": ["teslajsonpy==0.11.5"], "requirements": ["teslajsonpy==0.18.3"],
"codeowners": ["@zabuldon", "@alandtse"], "codeowners": ["@zabuldon", "@alandtse"],
"dhcp": [ "dhcp": [
{ {

View File

@ -2223,7 +2223,7 @@ temperusb==1.5.3
tesla-powerwall==0.3.5 tesla-powerwall==0.3.5
# homeassistant.components.tesla # homeassistant.components.tesla
teslajsonpy==0.11.5 teslajsonpy==0.18.3
# homeassistant.components.tensorflow # homeassistant.components.tensorflow
# tf-models-official==2.3.0 # tf-models-official==2.3.0

View File

@ -1180,7 +1180,7 @@ tellduslive==0.10.11
tesla-powerwall==0.3.5 tesla-powerwall==0.3.5
# homeassistant.components.tesla # homeassistant.components.tesla
teslajsonpy==0.11.5 teslajsonpy==0.18.3
# homeassistant.components.toon # homeassistant.components.toon
toonapi==0.2.0 toonapi==0.2.0

View File

@ -1,10 +1,12 @@
"""Test the Tesla config flow.""" """Test the Tesla config flow."""
import datetime
from unittest.mock import patch from unittest.mock import patch
from teslajsonpy import TeslaException from teslajsonpy.exceptions import IncompleteCredentials, TeslaException
from homeassistant import config_entries, data_entry_flow, setup from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.tesla.const import ( from homeassistant.components.tesla.const import (
CONF_EXPIRATION,
CONF_WAKE_ON_START, CONF_WAKE_ON_START,
DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
DEFAULT_WAKE_ON_START, DEFAULT_WAKE_ON_START,
@ -22,6 +24,12 @@ from homeassistant.const import (
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
TEST_USERNAME = "test-username"
TEST_TOKEN = "test-token"
TEST_PASSWORD = "test-password"
TEST_ACCESS_TOKEN = "test-access-token"
TEST_VALID_EXPIRATION = datetime.datetime.now().timestamp() * 2
async def test_form(hass): async def test_form(hass):
"""Test we get the form.""" """Test we get the form."""
@ -34,7 +42,11 @@ async def test_form(hass):
with patch( with patch(
"homeassistant.components.tesla.config_flow.TeslaAPI.connect", "homeassistant.components.tesla.config_flow.TeslaAPI.connect",
return_value=("test-refresh-token", "test-access-token"), return_value={
"refresh_token": TEST_TOKEN,
CONF_ACCESS_TOKEN: TEST_ACCESS_TOKEN,
CONF_EXPIRATION: TEST_VALID_EXPIRATION,
},
), patch( ), patch(
"homeassistant.components.tesla.async_setup", return_value=True "homeassistant.components.tesla.async_setup", return_value=True
) as mock_setup, patch( ) as mock_setup, patch(
@ -50,8 +62,9 @@ async def test_form(hass):
assert result2["data"] == { assert result2["data"] == {
CONF_USERNAME: "test@email.com", CONF_USERNAME: "test@email.com",
CONF_PASSWORD: "test", CONF_PASSWORD: "test",
CONF_TOKEN: "test-refresh-token", CONF_TOKEN: TEST_TOKEN,
CONF_ACCESS_TOKEN: "test-access-token", CONF_ACCESS_TOKEN: TEST_ACCESS_TOKEN,
CONF_EXPIRATION: TEST_VALID_EXPIRATION,
} }
assert len(mock_setup.mock_calls) == 1 assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
@ -69,7 +82,26 @@ async def test_form_invalid_auth(hass):
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, {CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "invalid_auth"}
async def test_form_invalid_auth_incomplete_credentials(hass):
"""Test we handle invalid auth with incomplete credentials."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.tesla.config_flow.TeslaAPI.connect",
side_effect=IncompleteCredentials(401),
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD},
) )
assert result2["type"] == "form" assert result2["type"] == "form"
@ -88,7 +120,7 @@ async def test_form_cannot_connect(hass):
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{CONF_PASSWORD: "test-password", CONF_USERNAME: "test-username"}, {CONF_PASSWORD: TEST_PASSWORD, CONF_USERNAME: TEST_USERNAME},
) )
assert result2["type"] == "form" assert result2["type"] == "form"
@ -99,8 +131,8 @@ async def test_form_repeat_identifier(hass):
"""Test we handle repeat identifiers.""" """Test we handle repeat identifiers."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
title="test-username", title=TEST_USERNAME,
data={"username": "test-username", "password": "test-password"}, data={"username": TEST_USERNAME, "password": TEST_PASSWORD},
options=None, options=None,
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
@ -110,11 +142,15 @@ async def test_form_repeat_identifier(hass):
) )
with patch( with patch(
"homeassistant.components.tesla.config_flow.TeslaAPI.connect", "homeassistant.components.tesla.config_flow.TeslaAPI.connect",
return_value=("test-refresh-token", "test-access-token"), return_value={
"refresh_token": TEST_TOKEN,
CONF_ACCESS_TOKEN: TEST_ACCESS_TOKEN,
CONF_EXPIRATION: TEST_VALID_EXPIRATION,
},
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, {CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD},
) )
assert result2["type"] == "abort" assert result2["type"] == "abort"
@ -125,8 +161,8 @@ async def test_form_reauth(hass):
"""Test we handle reauth.""" """Test we handle reauth."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
title="test-username", title=TEST_USERNAME,
data={"username": "test-username", "password": "same"}, data={"username": TEST_USERNAME, "password": "same"},
options=None, options=None,
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
@ -134,15 +170,19 @@ async def test_form_reauth(hass):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_REAUTH}, context={"source": config_entries.SOURCE_REAUTH},
data={"username": "test-username"}, data={"username": TEST_USERNAME},
) )
with patch( with patch(
"homeassistant.components.tesla.config_flow.TeslaAPI.connect", "homeassistant.components.tesla.config_flow.TeslaAPI.connect",
return_value=("test-refresh-token", "test-access-token"), return_value={
"refresh_token": TEST_TOKEN,
CONF_ACCESS_TOKEN: TEST_ACCESS_TOKEN,
CONF_EXPIRATION: TEST_VALID_EXPIRATION,
},
): ):
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, {CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: "new-password"},
) )
assert result2["type"] == "abort" assert result2["type"] == "abort"
@ -154,17 +194,21 @@ async def test_import(hass):
with patch( with patch(
"homeassistant.components.tesla.config_flow.TeslaAPI.connect", "homeassistant.components.tesla.config_flow.TeslaAPI.connect",
return_value=("test-refresh-token", "test-access-token"), return_value={
"refresh_token": TEST_TOKEN,
CONF_ACCESS_TOKEN: TEST_ACCESS_TOKEN,
CONF_EXPIRATION: TEST_VALID_EXPIRATION,
},
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_IMPORT}, context={"source": config_entries.SOURCE_IMPORT},
data={CONF_PASSWORD: "test-password", CONF_USERNAME: "test-username"}, data={CONF_PASSWORD: TEST_PASSWORD, CONF_USERNAME: TEST_USERNAME},
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "test-username" assert result["title"] == TEST_USERNAME
assert result["data"][CONF_ACCESS_TOKEN] == "test-access-token" assert result["data"][CONF_ACCESS_TOKEN] == TEST_ACCESS_TOKEN
assert result["data"][CONF_TOKEN] == "test-refresh-token" assert result["data"][CONF_TOKEN] == TEST_TOKEN
assert result["description_placeholders"] is None assert result["description_placeholders"] is None