From cf9cab900e3555ea3cee1f09a03b45ed76750105 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 21 Jun 2022 07:36:13 -0700 Subject: [PATCH] Allow multiple configuration entries for nest integration (#73720) * Add multiple config entry support for Nest * Set a config entry unique id based on nest project id * Add missing translations and remove untested committed * Remove unnecessary translation * Remove dead code * Remove old handling to avoid duplicate error logs --- homeassistant/components/nest/__init__.py | 40 ++++++------- homeassistant/components/nest/camera_sdm.py | 4 +- homeassistant/components/nest/climate_sdm.py | 4 +- homeassistant/components/nest/config_flow.py | 17 +++--- homeassistant/components/nest/device_info.py | 30 +++++++++- .../components/nest/device_trigger.py | 46 ++++---------- homeassistant/components/nest/diagnostics.py | 9 ++- homeassistant/components/nest/media_source.py | 27 +++------ homeassistant/components/nest/sensor_sdm.py | 4 +- homeassistant/components/nest/strings.json | 2 +- tests/components/nest/conftest.py | 10 +++- tests/components/nest/test_config_flow_sdm.py | 60 +++++++++++++------ tests/components/nest/test_init_sdm.py | 24 ++++++-- 13 files changed, 161 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 29c2d817acd..0e0128136ad 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -77,9 +77,6 @@ from .media_source import ( _LOGGER = logging.getLogger(__name__) -DATA_NEST_UNAVAILABLE = "nest_unavailable" - -NEST_SETUP_NOTIFICATION = "nest_setup" SENSOR_SCHEMA = vol.Schema( {vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list)} @@ -179,13 +176,16 @@ class SignalUpdateCallback: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nest from a config entry with dispatch between old/new flows.""" - config_mode = config_flow.get_config_mode(hass) if config_mode == config_flow.ConfigMode.LEGACY: return await async_setup_legacy_entry(hass, entry) if config_mode == config_flow.ConfigMode.SDM: await async_import_config(hass, entry) + elif entry.unique_id != entry.data[CONF_PROJECT_ID]: + hass.config_entries.async_update_entry( + entry, unique_id=entry.data[CONF_PROJECT_ID] + ) subscriber = await api.new_subscriber(hass, entry) if not subscriber: @@ -205,31 +205,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await subscriber.start_async() except AuthException as err: - _LOGGER.debug("Subscriber authentication error: %s", err) - raise ConfigEntryAuthFailed from err + raise ConfigEntryAuthFailed( + f"Subscriber authentication error: {str(err)}" + ) from err except ConfigurationException as err: _LOGGER.error("Configuration error: %s", err) subscriber.stop_async() return False except SubscriberException as err: - if DATA_NEST_UNAVAILABLE not in hass.data[DOMAIN]: - _LOGGER.error("Subscriber error: %s", err) - hass.data[DOMAIN][DATA_NEST_UNAVAILABLE] = True subscriber.stop_async() - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady(f"Subscriber error: {str(err)}") from err try: device_manager = await subscriber.async_get_device_manager() except ApiException as err: - if DATA_NEST_UNAVAILABLE not in hass.data[DOMAIN]: - _LOGGER.error("Device manager error: %s", err) - hass.data[DOMAIN][DATA_NEST_UNAVAILABLE] = True subscriber.stop_async() - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady(f"Device manager error: {str(err)}") from err - hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) - hass.data[DOMAIN][DATA_SUBSCRIBER] = subscriber - hass.data[DOMAIN][DATA_DEVICE_MANAGER] = device_manager + hass.data[DOMAIN][entry.entry_id] = { + DATA_SUBSCRIBER: subscriber, + DATA_DEVICE_MANAGER: device_manager, + } hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -252,7 +248,9 @@ async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None: CONF_SUBSCRIBER_ID_IMPORTED: True, # Don't delete user managed subscriber } ) - hass.config_entries.async_update_entry(entry, data=new_data) + hass.config_entries.async_update_entry( + entry, data=new_data, unique_id=new_data[CONF_PROJECT_ID] + ) if entry.data["auth_implementation"] == INSTALLED_AUTH_DOMAIN: # App Auth credentials have been deprecated and must be re-created @@ -288,13 +286,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Legacy API return True _LOGGER.debug("Stopping nest subscriber") - subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] + subscriber = hass.data[DOMAIN][entry.entry_id][DATA_SUBSCRIBER] subscriber.stop_async() unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN].pop(DATA_SUBSCRIBER) - hass.data[DOMAIN].pop(DATA_DEVICE_MANAGER) - hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index a089163a826..4e38338aee8 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -45,7 +45,9 @@ async def async_setup_sdm_entry( ) -> None: """Set up the cameras.""" - device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] + device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ + DATA_DEVICE_MANAGER + ] entities = [] for device in device_manager.devices.values(): if ( diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 6ee988b714f..452c30073da 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -82,7 +82,9 @@ async def async_setup_sdm_entry( ) -> None: """Set up the client entities.""" - device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] + device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ + DATA_DEVICE_MANAGER + ] entities = [] for device in device_manager.devices.values(): if ThermostatHvacTrait.NAME in device.traits: diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index bacd61447f5..479a54edbc7 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -276,10 +276,6 @@ class NestFlowHandler( if self.config_mode == ConfigMode.LEGACY: return await self.async_step_init(user_input) self._data[DATA_SDM] = {} - # Reauth will update an existing entry - entries = self._async_current_entries() - if entries and self.source != SOURCE_REAUTH: - return self.async_abort(reason="single_instance_allowed") if self.source == SOURCE_REAUTH: return await super().async_step_user(user_input) # Application Credentials setup needs information from the user @@ -339,13 +335,16 @@ class NestFlowHandler( """Collect device access project from user input.""" errors = {} if user_input is not None: - if user_input[CONF_PROJECT_ID] == self._data[CONF_CLOUD_PROJECT_ID]: + project_id = user_input[CONF_PROJECT_ID] + if project_id == self._data[CONF_CLOUD_PROJECT_ID]: _LOGGER.error( "Device Access Project ID and Cloud Project ID must not be the same, see documentation" ) errors[CONF_PROJECT_ID] = "wrong_project_id" else: self._data.update(user_input) + await self.async_set_unique_id(project_id) + self._abort_if_unique_id_configured() return await super().async_step_user() return self.async_show_form( @@ -465,13 +464,11 @@ class NestFlowHandler( async def async_step_finish(self, data: dict[str, Any] | None = None) -> FlowResult: """Create an entry for the SDM flow.""" assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" - await self.async_set_unique_id(DOMAIN) - # Update existing config entry when in the reauth flow. This - # integration only supports one config entry so remove any prior entries - # added before the "single_instance_allowed" check was added + # Update existing config entry when in the reauth flow. if entry := self._async_reauth_entry(): self.hass.config_entries.async_update_entry( - entry, data=self._data, unique_id=DOMAIN + entry, + data=self._data, ) await self.hass.config_entries.async_reload(entry.entry_id) return self.async_abort(reason="reauth_successful") diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index b9aa52aa2c6..2d2b01d3849 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -2,12 +2,16 @@ from __future__ import annotations +from collections.abc import Mapping + from google_nest_sdm.device import Device from google_nest_sdm.device_traits import InfoTrait +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo -from .const import DOMAIN +from .const import DATA_DEVICE_MANAGER, DOMAIN DEVICE_TYPE_MAP: dict[str, str] = { "sdm.devices.types.CAMERA": "Camera", @@ -66,3 +70,27 @@ class NestDeviceInfo: names = [name for id, name in items] return " ".join(names) return None + + +@callback +def async_nest_devices(hass: HomeAssistant) -> Mapping[str, Device]: + """Return a mapping of all nest devices for all config entries.""" + devices = {} + for entry_id in hass.data[DOMAIN]: + if not (device_manager := hass.data[DOMAIN][entry_id].get(DATA_DEVICE_MANAGER)): + continue + devices.update( + {device.name: device for device in device_manager.devices.values()} + ) + return devices + + +@callback +def async_nest_devices_by_device_id(hass: HomeAssistant) -> Mapping[str, Device]: + """Return a mapping of all nest devices by home assistant device id, for all config entries.""" + device_registry = dr.async_get(hass) + devices = {} + for nest_device_id, device in async_nest_devices(hass).items(): + if device_entry := device_registry.async_get_device({(DOMAIN, nest_device_id)}): + devices[device_entry.id] = device + return devices diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index 05769a407f2..cb546c87ee4 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -1,7 +1,6 @@ """Provides device automations for Nest.""" from __future__ import annotations -from google_nest_sdm.device_manager import DeviceManager import voluptuous as vol from homeassistant.components.automation import ( @@ -14,11 +13,11 @@ from homeassistant.components.device_automation.exceptions import ( ) from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers import device_registry as dr +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.typing import ConfigType -from .const import DATA_DEVICE_MANAGER, DOMAIN +from .const import DOMAIN +from .device_info import async_nest_devices_by_device_id from .events import DEVICE_TRAIT_TRIGGER_MAP, NEST_EVENT DEVICE = "device" @@ -32,43 +31,18 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( ) -@callback -def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str | None: - """Get the nest API device_id from the HomeAssistant device_id.""" - device_registry = dr.async_get(hass) - if device := device_registry.async_get(device_id): - for (domain, unique_id) in device.identifiers: - if domain == DOMAIN: - return unique_id - return None - - -@callback -def async_get_device_trigger_types( - hass: HomeAssistant, nest_device_id: str -) -> list[str]: - """List event triggers supported for a Nest device.""" - device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] - if not (nest_device := device_manager.devices.get(nest_device_id)): - raise InvalidDeviceAutomationConfig(f"Nest device not found {nest_device_id}") - - # Determine the set of event types based on the supported device traits - trigger_types = [ - trigger_type - for trait in nest_device.traits - if (trigger_type := DEVICE_TRAIT_TRIGGER_MAP.get(trait)) - ] - return trigger_types - - async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device triggers for a Nest device.""" - nest_device_id = async_get_nest_device_id(hass, device_id) - if not nest_device_id: + devices = async_nest_devices_by_device_id(hass) + if not (device := devices.get(device_id)): raise InvalidDeviceAutomationConfig(f"Device not found {device_id}") - trigger_types = async_get_device_trigger_types(hass, nest_device_id) + trigger_types = [ + trigger_type + for trait in device.traits + if (trigger_type := DEVICE_TRAIT_TRIGGER_MAP.get(trait)) + ] return [ { CONF_PLATFORM: DEVICE, diff --git a/homeassistant/components/nest/diagnostics.py b/homeassistant/components/nest/diagnostics.py index c21842d5939..d350b719608 100644 --- a/homeassistant/components/nest/diagnostics.py +++ b/homeassistant/components/nest/diagnostics.py @@ -27,10 +27,15 @@ def _async_get_nest_devices( if DATA_SDM not in config_entry.data: return {} - if DATA_DEVICE_MANAGER not in hass.data[DOMAIN]: + if ( + config_entry.entry_id not in hass.data[DOMAIN] + or DATA_DEVICE_MANAGER not in hass.data[DOMAIN][config_entry.entry_id] + ): return {} - device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] + device_manager: DeviceManager = hass.data[DOMAIN][config_entry.entry_id][ + DATA_DEVICE_MANAGER + ] return device_manager.devices diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index e4e26153b3a..4614d4b1ed4 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -25,7 +25,6 @@ import os from google_nest_sdm.camera_traits import CameraClipPreviewTrait, CameraEventImageTrait from google_nest_sdm.device import Device -from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.event import EventImageType, ImageEventBase from google_nest_sdm.event_media import ( ClipPreviewSession, @@ -57,8 +56,8 @@ from homeassistant.helpers.storage import Store from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.util import dt as dt_util -from .const import DATA_DEVICE_MANAGER, DOMAIN -from .device_info import NestDeviceInfo +from .const import DOMAIN +from .device_info import NestDeviceInfo, async_nest_devices_by_device_id from .events import EVENT_NAME_MAP, MEDIA_SOURCE_EVENT_TITLE_MAP _LOGGER = logging.getLogger(__name__) @@ -271,21 +270,13 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource: @callback def async_get_media_source_devices(hass: HomeAssistant) -> Mapping[str, Device]: """Return a mapping of device id to eligible Nest event media devices.""" - if DATA_DEVICE_MANAGER not in hass.data[DOMAIN]: - # Integration unloaded, or is legacy nest integration - return {} - device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] - device_registry = dr.async_get(hass) - devices = {} - for device in device_manager.devices.values(): - if not ( - CameraEventImageTrait.NAME in device.traits - or CameraClipPreviewTrait.NAME in device.traits - ): - continue - if device_entry := device_registry.async_get_device({(DOMAIN, device.name)}): - devices[device_entry.id] = device - return devices + devices = async_nest_devices_by_device_id(hass) + return { + device_id: device + for device_id, device in devices.items() + if CameraEventImageTrait.NAME in device.traits + or CameraClipPreviewTrait.NAME in device.traits + } @dataclass diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index d33aa3eff8b..c6d1c8b2b30 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -36,7 +36,9 @@ async def async_setup_sdm_entry( ) -> None: """Set up the sensors.""" - device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] + device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ + DATA_DEVICE_MANAGER + ] entities: list[SensorEntity] = [] for device in device_manager.devices.values(): if TemperatureTrait.NAME in device.traits: diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 212903179b7..0a13de41511 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -69,7 +69,7 @@ "subscriber_error": "Unknown subscriber error, see logs" }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]", diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index bacb3924bcd..458685cde70 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -24,6 +24,7 @@ from homeassistant.setup import async_setup_component from .common import ( DEVICE_ID, + PROJECT_ID, SUBSCRIBER_ID, TEST_CONFIG_APP_CREDS, TEST_CONFIG_YAML_ONLY, @@ -213,11 +214,18 @@ def config( return config +@pytest.fixture +def config_entry_unique_id() -> str: + """Fixture to set ConfigEntry unique id.""" + return PROJECT_ID + + @pytest.fixture def config_entry( subscriber_id: str | None, auth_implementation: str | None, nest_test_config: NestTestConfig, + config_entry_unique_id: str, ) -> MockConfigEntry | None: """Fixture that sets up the ConfigEntry for the test.""" if nest_test_config.config_entry_data is None: @@ -229,7 +237,7 @@ def config_entry( else: del data[CONF_SUBSCRIBER_ID] data["auth_implementation"] = auth_implementation - return MockConfigEntry(domain=DOMAIN, data=data) + return MockConfigEntry(domain=DOMAIN, data=data, unique_id=config_entry_unique_id) @pytest.fixture(autouse=True) diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index f4299808bf0..53a2d9cf2b6 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -76,8 +76,8 @@ class OAuthFixture: assert result.get("type") == "form" assert result.get("step_id") == "device_project" - result = await self.async_configure(result, {"project_id": PROJECT_ID}) - await self.async_oauth_web_flow(result) + result = await self.async_configure(result, {"project_id": project_id}) + await self.async_oauth_web_flow(result, project_id=project_id) async def async_oauth_web_flow(self, result: dict, project_id=PROJECT_ID) -> None: """Invoke the oauth flow for Web Auth with fake responses.""" @@ -404,7 +404,7 @@ async def test_web_reauth(hass, oauth, setup_platform, config_entry): entry = await oauth.async_finish_setup(result) # Verify existing tokens are replaced entry.data["token"].pop("expires_at") - assert entry.unique_id == DOMAIN + assert entry.unique_id == PROJECT_ID assert entry.data["token"] == { "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", @@ -415,25 +415,51 @@ async def test_web_reauth(hass, oauth, setup_platform, config_entry): assert entry.data.get("subscriber_id") == orig_subscriber_id # Not updated -async def test_single_config_entry(hass, setup_platform): - """Test that only a single config entry is allowed.""" +async def test_multiple_config_entries(hass, oauth, setup_platform): + """Verify config flow can be started when existing config entry exists.""" await setup_platform() + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" - assert result["reason"] == "single_instance_allowed" + await oauth.async_app_creds_flow(result, project_id="project-id-2") + entry = await oauth.async_finish_setup(result) + assert entry.title == "Mock Title" + assert "token" in entry.data + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 2 -async def test_unexpected_existing_config_entries( +async def test_duplicate_config_entries(hass, oauth, setup_platform): + """Verify that config entries must be for unique projects.""" + await setup_platform() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "form" + assert result.get("step_id") == "cloud_project" + + result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) + assert result.get("type") == "form" + assert result.get("step_id") == "device_project" + + result = await oauth.async_configure(result, {"project_id": PROJECT_ID}) + assert result.get("type") == "abort" + assert result.get("reason") == "already_configured" + + +async def test_reauth_multiple_config_entries( hass, oauth, setup_platform, config_entry ): """Test Nest reauthentication with multiple existing config entries.""" - # Note that this case will not happen in the future since only a single - # instance is now allowed, but this may have been allowed in the past. - # On reauth, only one entry is kept and the others are deleted. - await setup_platform() old_entry = MockConfigEntry( @@ -461,7 +487,7 @@ async def test_unexpected_existing_config_entries( entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 2 entry = entries[0] - assert entry.unique_id == DOMAIN + assert entry.unique_id == PROJECT_ID entry.data["token"].pop("expires_at") assert entry.data["token"] == { "refresh_token": "mock-refresh-token", @@ -540,7 +566,7 @@ async def test_app_auth_yaml_reauth(hass, oauth, setup_platform, config_entry): # Verify existing tokens are replaced entry = await oauth.async_finish_setup(result, {"code": "1234"}) entry.data["token"].pop("expires_at") - assert entry.unique_id == DOMAIN + assert entry.unique_id == PROJECT_ID assert entry.data["token"] == { "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", @@ -570,7 +596,7 @@ async def test_web_auth_yaml_reauth(hass, oauth, setup_platform, config_entry): # Verify existing tokens are replaced entry = await oauth.async_finish_setup(result, {"code": "1234"}) entry.data["token"].pop("expires_at") - assert entry.unique_id == DOMAIN + assert entry.unique_id == PROJECT_ID assert entry.data["token"] == { "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", @@ -599,7 +625,7 @@ async def test_pubsub_subscription_strip_whitespace( assert entry.title == "Import from configuration.yaml" assert "token" in entry.data entry.data["token"].pop("expires_at") - assert entry.unique_id == DOMAIN + assert entry.unique_id == PROJECT_ID assert entry.data["token"] == { "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", @@ -643,7 +669,7 @@ async def test_pubsub_subscriber_config_entry_reauth( # Entering an updated access token refreshs the config entry. entry = await oauth.async_finish_setup(result, {"code": "1234"}) entry.data["token"].pop("expires_at") - assert entry.unique_id == DOMAIN + assert entry.unique_id == PROJECT_ID assert entry.data["token"] == { "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", diff --git a/tests/components/nest/test_init_sdm.py b/tests/components/nest/test_init_sdm.py index 1b473ccd62f..d7c82609c60 100644 --- a/tests/components/nest/test_init_sdm.py +++ b/tests/components/nest/test_init_sdm.py @@ -103,18 +103,18 @@ async def test_setup_configuration_failure( @pytest.mark.parametrize("subscriber_side_effect", [SubscriberException()]) async def test_setup_susbcriber_failure( - hass, error_caplog, failing_subscriber, setup_base_platform + hass, warning_caplog, failing_subscriber, setup_base_platform ): """Test configuration error.""" await setup_base_platform() - assert "Subscriber error:" in error_caplog.text + assert "Subscriber error:" in warning_caplog.text entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 assert entries[0].state is ConfigEntryState.SETUP_RETRY -async def test_setup_device_manager_failure(hass, error_caplog, setup_base_platform): +async def test_setup_device_manager_failure(hass, warning_caplog, setup_base_platform): """Test device manager api failure.""" with patch( "homeassistant.components.nest.api.GoogleNestSubscriber.start_async" @@ -124,8 +124,7 @@ async def test_setup_device_manager_failure(hass, error_caplog, setup_base_platf ): await setup_base_platform() - assert len(error_caplog.messages) == 1 - assert "Device manager error:" in error_caplog.text + assert "Device manager error:" in warning_caplog.text entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 @@ -273,3 +272,18 @@ async def test_remove_entry_delete_subscriber_failure( entries = hass.config_entries.async_entries(DOMAIN) assert not entries + + +@pytest.mark.parametrize("config_entry_unique_id", [DOMAIN, None]) +async def test_migrate_unique_id( + hass, error_caplog, setup_platform, config_entry, config_entry_unique_id +): + """Test successful setup.""" + + assert config_entry.state is ConfigEntryState.NOT_LOADED + assert config_entry.unique_id == config_entry_unique_id + + await setup_platform() + + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.unique_id == PROJECT_ID