Add unique IDs to config entries for Teslemetry (#115616)

* Add basic UID

* Add Unique IDs

* Add debug message

* Readd debug message

* Minor bump config version

* Ruff

* Rework migration

* Fix migration return

* Review feedback

* Add test for v2
This commit is contained in:
Brett Adams 2024-06-15 20:10:02 +10:00 committed by GitHub
parent a515562a11
commit dac661831e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 128 additions and 26 deletions

View file

@ -18,7 +18,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from .const import DOMAIN, MODELS
from .const import DOMAIN, LOGGER, MODELS
from .coordinator import (
TeslemetryEnergySiteInfoCoordinator,
TeslemetryEnergySiteLiveCoordinator,
@ -153,3 +153,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
async def async_unload_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -> bool:
"""Unload Teslemetry Config."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate config entry."""
if config_entry.version > 1:
return False
if config_entry.version == 1 and config_entry.minor_version < 2:
# Add unique_id to existing entry
teslemetry = Teslemetry(
session=async_get_clientsession(hass),
access_token=config_entry.data[CONF_ACCESS_TOKEN],
)
try:
metadata = await teslemetry.metadata()
except TeslaFleetError as e:
LOGGER.error(e.message)
return False
hass.config_entries.async_update_entry(
config_entry, unique_id=metadata["uid"], version=1, minor_version=2
)
return True

View file

@ -31,6 +31,7 @@ class TeslemetryConfigFlow(ConfigFlow, domain=DOMAIN):
"""Config Teslemetry API connection."""
VERSION = 1
MINOR_VERSION = 2
_entry: ConfigEntry | None = None
async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]:
@ -40,7 +41,7 @@ class TeslemetryConfigFlow(ConfigFlow, domain=DOMAIN):
access_token=user_input[CONF_ACCESS_TOKEN],
)
try:
await teslemetry.test()
metadata = await teslemetry.metadata()
except InvalidToken:
return {CONF_ACCESS_TOKEN: "invalid_access_token"}
except SubscriptionRequired:
@ -50,6 +51,8 @@ class TeslemetryConfigFlow(ConfigFlow, domain=DOMAIN):
except TeslaFleetError as e:
LOGGER.error(e)
return {"base": "unknown"}
await self.async_set_unique_id(metadata["uid"])
return {}
async def async_step_user(

View file

@ -1,5 +1,8 @@
{
"config": {
"abort": {
"already_configured": "Account is already configured"
},
"error": {
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
"subscription_required": "Subscription required, please visit {short_url}",

View file

@ -18,8 +18,7 @@ async def setup_platform(hass: HomeAssistant, platforms: list[Platform] | None =
"""Set up the Teslemetry platform."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
data=CONFIG,
domain=DOMAIN, data=CONFIG, minor_version=2, unique_id="abc-123"
)
mock_entry.add_to_hass(hass)

View file

@ -31,6 +31,7 @@ COMMAND_ERRORS = (COMMAND_REASON, COMMAND_NOREASON, COMMAND_ERROR, COMMAND_NOERR
RESPONSE_OK = {"response": {}, "error": None}
METADATA = {
"uid": "abc-123",
"region": "NA",
"scopes": [
"openid",
@ -44,6 +45,7 @@ METADATA = {
],
}
METADATA_NOSCOPE = {
"uid": "abc-123",
"region": "NA",
"scopes": ["openid", "offline_access", "vehicle_device_data"],
}

View file

@ -12,26 +12,18 @@ from tesla_fleet_api.exceptions import (
from homeassistant import config_entries
from homeassistant.components.teslemetry.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from .const import CONFIG
from .const import CONFIG, METADATA
from tests.common import MockConfigEntry
BAD_CONFIG = {CONF_ACCESS_TOKEN: "bad_access_token"}
@pytest.fixture(autouse=True)
def mock_test():
"""Mock Teslemetry api class."""
with patch(
"homeassistant.components.teslemetry.Teslemetry.test", return_value=True
) as mock_test:
yield mock_test
async def test_form(
hass: HomeAssistant,
) -> None:
@ -67,14 +59,16 @@ async def test_form(
(TeslaFleetError, {"base": "unknown"}),
],
)
async def test_form_errors(hass: HomeAssistant, side_effect, error, mock_test) -> None:
async def test_form_errors(
hass: HomeAssistant, side_effect, error, mock_metadata
) -> None:
"""Test errors are handled."""
result1 = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
mock_test.side_effect = side_effect
mock_metadata.side_effect = side_effect
result2 = await hass.config_entries.flow.async_configure(
result1["flow_id"],
CONFIG,
@ -84,7 +78,7 @@ async def test_form_errors(hass: HomeAssistant, side_effect, error, mock_test) -
assert result2["errors"] == error
# Complete the flow
mock_test.side_effect = None
mock_metadata.side_effect = None
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
CONFIG,
@ -92,12 +86,11 @@ async def test_form_errors(hass: HomeAssistant, side_effect, error, mock_test) -
assert result3["type"] is FlowResultType.CREATE_ENTRY
async def test_reauth(hass: HomeAssistant, mock_test) -> None:
async def test_reauth(hass: HomeAssistant, mock_metadata) -> None:
"""Test reauth flow."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
data=BAD_CONFIG,
domain=DOMAIN, data=BAD_CONFIG, minor_version=2, unique_id="abc-123"
)
mock_entry.add_to_hass(hass)
@ -124,7 +117,7 @@ async def test_reauth(hass: HomeAssistant, mock_test) -> None:
)
await hass.async_block_till_done()
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_test.mock_calls) == 1
assert len(mock_metadata.mock_calls) == 1
assert result2["type"] is FlowResultType.ABORT
assert result2["reason"] == "reauth_successful"
@ -141,14 +134,13 @@ async def test_reauth(hass: HomeAssistant, mock_test) -> None:
],
)
async def test_reauth_errors(
hass: HomeAssistant, mock_test, side_effect, error
hass: HomeAssistant, mock_metadata, side_effect, error
) -> None:
"""Test reauth flows that fail."""
# Start the reauth
mock_entry = MockConfigEntry(
domain=DOMAIN,
data=BAD_CONFIG,
domain=DOMAIN, data=BAD_CONFIG, minor_version=2, unique_id="abc-123"
)
mock_entry.add_to_hass(hass)
@ -162,7 +154,7 @@ async def test_reauth_errors(
data=BAD_CONFIG,
)
mock_test.side_effect = side_effect
mock_metadata.side_effect = side_effect
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
BAD_CONFIG,
@ -173,7 +165,7 @@ async def test_reauth_errors(
assert result2["errors"] == error
# Complete the flow
mock_test.side_effect = None
mock_metadata.side_effect = None
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
CONFIG,
@ -182,3 +174,83 @@ async def test_reauth_errors(
assert result3["type"] is FlowResultType.ABORT
assert result3["reason"] == "reauth_successful"
assert mock_entry.data == CONFIG
async def test_unique_id_abort(
hass: HomeAssistant,
) -> None:
"""Test duplicate unique ID in config."""
result1 = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG
)
assert result1["type"] is FlowResultType.CREATE_ENTRY
# Setup a duplicate
result2 = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG
)
assert result2["type"] is FlowResultType.ABORT
async def test_migrate_from_1_1(hass: HomeAssistant, mock_metadata) -> None:
"""Test config migration."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
version=1,
minor_version=1,
unique_id=None,
data=CONFIG,
)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
entry = hass.config_entries.async_get_entry(mock_entry.entry_id)
assert entry.version == 1
assert entry.minor_version == 2
assert entry.unique_id == METADATA["uid"]
async def test_migrate_error_from_1_1(hass: HomeAssistant, mock_metadata) -> None:
"""Test config migration handles errors."""
mock_metadata.side_effect = TeslaFleetError
mock_entry = MockConfigEntry(
domain=DOMAIN,
version=1,
minor_version=1,
unique_id=None,
data=CONFIG,
)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
entry = hass.config_entries.async_get_entry(mock_entry.entry_id)
assert entry.state is ConfigEntryState.MIGRATION_ERROR
async def test_migrate_error_from_future(hass: HomeAssistant, mock_metadata) -> None:
"""Test a future version isn't migrated."""
mock_metadata.side_effect = TeslaFleetError
mock_entry = MockConfigEntry(
domain=DOMAIN,
version=2,
minor_version=1,
unique_id="abc-123",
data=CONFIG,
)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
entry = hass.config_entries.async_get_entry(mock_entry.entry_id)
assert entry.state is ConfigEntryState.MIGRATION_ERROR