From dac661831e837cb8e22acdfb8ef2347938849254 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sat, 15 Jun 2024 20:10:02 +1000 Subject: [PATCH] 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 --- .../components/teslemetry/__init__.py | 25 +++- .../components/teslemetry/config_flow.py | 5 +- .../components/teslemetry/strings.json | 3 + tests/components/teslemetry/__init__.py | 3 +- tests/components/teslemetry/const.py | 2 + .../components/teslemetry/test_config_flow.py | 116 ++++++++++++++---- 6 files changed, 128 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/teslemetry/__init__.py b/homeassistant/components/teslemetry/__init__.py index 387ebd1039e..21ea2915884 100644 --- a/homeassistant/components/teslemetry/__init__.py +++ b/homeassistant/components/teslemetry/__init__.py @@ -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 diff --git a/homeassistant/components/teslemetry/config_flow.py b/homeassistant/components/teslemetry/config_flow.py index 5fb6ce56aed..73921986f44 100644 --- a/homeassistant/components/teslemetry/config_flow.py +++ b/homeassistant/components/teslemetry/config_flow.py @@ -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( diff --git a/homeassistant/components/teslemetry/strings.json b/homeassistant/components/teslemetry/strings.json index d3740db9760..fe45b4ee9e3 100644 --- a/homeassistant/components/teslemetry/strings.json +++ b/homeassistant/components/teslemetry/strings.json @@ -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}", diff --git a/tests/components/teslemetry/__init__.py b/tests/components/teslemetry/__init__.py index daa2c070091..c4fbdaf3fbd 100644 --- a/tests/components/teslemetry/__init__.py +++ b/tests/components/teslemetry/__init__.py @@ -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) diff --git a/tests/components/teslemetry/const.py b/tests/components/teslemetry/const.py index ffb349e4b7e..6a3a657a1b1 100644 --- a/tests/components/teslemetry/const.py +++ b/tests/components/teslemetry/const.py @@ -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"], } diff --git a/tests/components/teslemetry/test_config_flow.py b/tests/components/teslemetry/test_config_flow.py index 2f12b202712..fa35142dc07 100644 --- a/tests/components/teslemetry/test_config_flow.py +++ b/tests/components/teslemetry/test_config_flow.py @@ -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