Add reauth to Tessie (#105419)

* First pass at Tessie

* Working POC

* async_step_reauth

* Config Flow tests

* WIP

* Add test requirement

* correctly gen test requirements

* 100% coverage

* Remove remnants of copy paste

* Add TPMS

* Fix docstring

* Remove redundant line

* Fix some more copied docstrings

* Grammar

* Create reusable StrEnum

* Streamline get

* Add a couple more sensors

* Removed need for a model

* Move MODELS

* Remove DOMAIN from config flow

* Add translation strings

* Remove unused parameter

* Simplify error handling

* Refactor coordinator to class

* Add missing types

* Add icon to shift state

* Simplify setdefault

Co-authored-by: J. Nick Koston <nick@koston.org>

* Use walrus for async_unload_platforms

Co-authored-by: J. Nick Koston <nick@koston.org>

* Reformat entity init

Co-authored-by: J. Nick Koston <nick@koston.org>

* Remove Unique ID

* Better Config Flow Tests

* Fix all remaining tests

* Standardise docstring

* Remove redudnant test

* Set TessieDataUpdateCoordinator on entity

* Correct some sensors

* add error types

* Make shift state a ENUM sensor

* Add more sensors

* Fix translation string

* Add precision suggestions

* Move session from init to coordinator

* Add api_key type

* Remove api_key parameter

* Meta changes

* Update TessieEntity and TessieSensor translations

* Goodbye translation keys

* bump tessie-api to 0.0.9

* Fix only_active to be True

* Per vehicle coordinator

* Rework coordinator

* Fix coverage

* WIP

* The grand rework

* Add comments

* Use ENUM more

* Add ENUM translations

* Update homeassistant/components/tessie/sensor.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Add entity_category

* Remove reauth

* Remove session

* Update homeassistant/components/tessie/__init__.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Add property tag

* Add error text

* Complete config flow tests

* Fix property and rename

* Fix init test

* Reworked coordinator tests

* Add extra checks

* Update homeassistant/components/tessie/__init__.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/tessie/coordinator.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Apply suggestions from code review

Co-authored-by: J. Nick Koston <nick@koston.org>

* Ruff fix

* Update homeassistant/components/tessie/config_flow.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Remove future ENUMs

Co-authored-by: J. Nick Koston <nick@koston.org>

* Ruff fix

* Update homeassistant/components/tessie/config_flow.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Remove reauth and already configured strings

* Lowercase sensor values for translation

* Update homeassistant/components/tessie/__init__.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Fixed, before using lambda

* Add value lambda

* fix lambda

* Fix config flow test

* @bdraco fix for 500 errors

* format

* Add reauth

* Reuse string in reauth

* Ruff

* remove redundant check

* Improve error tests

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Brett Adams 2023-12-10 17:37:57 +10:00 committed by GitHub
parent 7b32e4142e
commit 1cc47c0553
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 185 additions and 7 deletions

View File

@ -1,4 +1,5 @@
"""Tessie integration."""
from http import HTTPStatus
import logging
from aiohttp import ClientError, ClientResponseError
@ -7,7 +8,7 @@ from tessie_api import get_state_of_all_vehicles
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
@ -28,9 +29,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
api_key=api_key,
only_active=True,
)
except ClientResponseError as ex:
# Reauth will go here
_LOGGER.error("Setup failed, unable to connect to Tessie: %s", ex)
except ClientResponseError as e:
if e.status == HTTPStatus.UNAUTHORIZED:
raise ConfigEntryAuthFailed from e
_LOGGER.error("Setup failed, unable to connect to Tessie: %s", e)
return False
except ClientError as e:
raise ConfigEntryNotReady from e

View File

@ -10,6 +10,7 @@ from tessie_api import get_state_of_all_vehicles
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -24,12 +25,16 @@ class TessieConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
def __init__(self) -> None:
"""Initialize."""
self._reauth_entry: ConfigEntry | None = None
async def async_step_user(
self, user_input: Mapping[str, Any] | None = None
) -> FlowResult:
"""Get configuration from the user."""
errors: dict[str, str] = {}
if user_input and CONF_ACCESS_TOKEN in user_input:
if user_input:
try:
await get_state_of_all_vehicles(
session=async_get_clientsession(self.hass),
@ -54,3 +59,44 @@ class TessieConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data_schema=TESSIE_SCHEMA,
errors=errors,
)
async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult:
"""Handle re-auth."""
self._reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: Mapping[str, Any] | None = None
) -> FlowResult:
"""Get update API Key from the user."""
errors: dict[str, str] = {}
assert self._reauth_entry
if user_input:
try:
await get_state_of_all_vehicles(
session=async_get_clientsession(self.hass),
api_key=user_input[CONF_ACCESS_TOKEN],
)
except ClientResponseError as e:
if e.status == HTTPStatus.UNAUTHORIZED:
errors["base"] = "invalid_access_token"
else:
errors["base"] = "unknown"
except ClientConnectionError:
errors["base"] = "cannot_connect"
else:
self.hass.config_entries.async_update_entry(
self._reauth_entry, data=user_input
)
self.hass.async_create_task(
self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
data_schema=TESSIE_SCHEMA,
errors=errors,
)

View File

@ -8,6 +8,7 @@ from aiohttp import ClientResponseError
from tessie_api import get_state
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
@ -57,7 +58,9 @@ class TessieDataUpdateCoordinator(DataUpdateCoordinator):
# Vehicle is offline, only update state and dont throw error
self.data["state"] = TessieStatus.OFFLINE
return self.data
# Reauth will go here
if e.status == HTTPStatus.UNAUTHORIZED:
# Auth Token is no longer valid
raise ConfigEntryAuthFailed from e
raise e
self.did_first_update = True

View File

@ -11,6 +11,13 @@
"access_token": "[%key:common::config_flow::data::access_token%]"
},
"description": "Enter your access token from [my.tessie.com/settings/api](https://my.tessie.com/settings/api)."
},
"reauth_confirm": {
"data": {
"access_token": "[%key:common::config_flow::data::access_token%]"
},
"description": "[%key:component::tessie::config::step::user::description%]",
"title": "[%key:common::config_flow::title::reauth%]"
}
}
},

View File

@ -2,6 +2,8 @@
from unittest.mock import patch
import pytest
from homeassistant import config_entries
from homeassistant.components.tessie.const import DOMAIN
from homeassistant.const import CONF_ACCESS_TOKEN
@ -14,8 +16,21 @@ from .common import (
ERROR_UNKNOWN,
TEST_CONFIG,
TEST_STATE_OF_ALL_VEHICLES,
setup_platform,
)
from tests.common import MockConfigEntry
@pytest.fixture
def mock_get_state_of_all_vehicles():
"""Mock get_state_of_all_vehicles function."""
with patch(
"homeassistant.components.tessie.config_flow.get_state_of_all_vehicles",
return_value=TEST_STATE_OF_ALL_VEHICLES,
) as mock_get_state_of_all_vehicles:
yield mock_get_state_of_all_vehicles
async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form."""
@ -137,3 +152,89 @@ async def test_form_network_issue(hass: HomeAssistant) -> None:
TEST_CONFIG,
)
assert result3["type"] == FlowResultType.CREATE_ENTRY
async def test_reauth(hass: HomeAssistant, mock_get_state_of_all_vehicles) -> None:
"""Test reauth flow."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
data=TEST_CONFIG,
)
mock_entry.add_to_hass(hass)
result1 = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": mock_entry.entry_id,
},
data=TEST_CONFIG,
)
assert result1["type"] == FlowResultType.FORM
assert result1["step_id"] == "reauth_confirm"
assert not result1["errors"]
with patch(
"homeassistant.components.tessie.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result1["flow_id"],
TEST_CONFIG,
)
await hass.async_block_till_done()
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_get_state_of_all_vehicles.mock_calls) == 1
assert result2["type"] == FlowResultType.ABORT
assert result2["reason"] == "reauth_successful"
assert mock_entry.data == TEST_CONFIG
@pytest.mark.parametrize(
("side_effect", "error"),
[
(ERROR_AUTH, {"base": "invalid_access_token"}),
(ERROR_UNKNOWN, {"base": "unknown"}),
(ERROR_CONNECTION, {"base": "cannot_connect"}),
],
)
async def test_reauth_errors(
hass: HomeAssistant, mock_get_state_of_all_vehicles, side_effect, error
) -> None:
"""Test reauth flows that failscript/."""
mock_entry = await setup_platform(hass)
mock_get_state_of_all_vehicles.side_effect = side_effect
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"unique_id": mock_entry.unique_id,
"entry_id": mock_entry.entry_id,
},
data=TEST_CONFIG,
)
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
TEST_CONFIG,
)
await hass.async_block_till_done()
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == error
# Complete the flow
mock_get_state_of_all_vehicles.side_effect = None
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
TEST_CONFIG,
)
assert "errors" not in result3
assert result3["type"] == FlowResultType.ABORT
assert result3["reason"] == "reauth_successful"
assert mock_entry.data == TEST_CONFIG

View File

@ -11,6 +11,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.util.dt import utcnow
from .common import (
ERROR_AUTH,
ERROR_CONNECTION,
ERROR_TIMEOUT,
ERROR_UNKNOWN,
@ -81,6 +82,17 @@ async def test_coordinator_timeout(hass: HomeAssistant, mock_get_state) -> None:
assert hass.states.get("sensor.test_status").state == TessieStatus.OFFLINE
async def test_coordinator_auth(hass: HomeAssistant, mock_get_state) -> None:
"""Tests that the coordinator handles timeout errors."""
mock_get_state.side_effect = ERROR_AUTH
await setup_platform(hass)
async_fire_time_changed(hass, utcnow() + WAIT)
await hass.async_block_till_done()
mock_get_state.assert_called_once()
async def test_coordinator_connection(hass: HomeAssistant, mock_get_state) -> None:
"""Tests that the coordinator handles connection errors."""

View File

@ -3,7 +3,7 @@
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from .common import ERROR_CONNECTION, ERROR_UNKNOWN, setup_platform
from .common import ERROR_AUTH, ERROR_CONNECTION, ERROR_UNKNOWN, setup_platform
async def test_load_unload(hass: HomeAssistant) -> None:
@ -16,6 +16,13 @@ async def test_load_unload(hass: HomeAssistant) -> None:
assert entry.state is ConfigEntryState.NOT_LOADED
async def test_auth_failure(hass: HomeAssistant) -> None:
"""Test init with an authentication failure."""
entry = await setup_platform(hass, side_effect=ERROR_AUTH)
assert entry.state is ConfigEntryState.SETUP_ERROR
async def test_unknown_failure(hass: HomeAssistant) -> None:
"""Test init with an authentication failure."""