Deprecate google calendar configuration.yaml (#72288)

* Deprecate google calendar configuration.yaml

* Remove unused translations

* Enable strict type checking and address pr feedback

* Move default hass.data init to `async_setup`
This commit is contained in:
Allen Porter 2022-05-22 14:29:11 -07:00 committed by GitHub
parent 9c3f949165
commit e6ffae8bd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 267 additions and 47 deletions

View File

@ -101,6 +101,7 @@ homeassistant.components.geo_location.*
homeassistant.components.geocaching.* homeassistant.components.geocaching.*
homeassistant.components.gios.* homeassistant.components.gios.*
homeassistant.components.goalzero.* homeassistant.components.goalzero.*
homeassistant.components.google.*
homeassistant.components.greeneye_monitor.* homeassistant.components.greeneye_monitor.*
homeassistant.components.group.* homeassistant.components.group.*
homeassistant.components.guardian.* homeassistant.components.guardian.*

View File

@ -97,23 +97,27 @@ PLATFORMS = ["calendar"]
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ vol.All(
DOMAIN: vol.Schema( cv.deprecated(DOMAIN),
{ {
vol.Required(CONF_CLIENT_ID): cv.string, DOMAIN: vol.Schema(
vol.Required(CONF_CLIENT_SECRET): cv.string, {
vol.Optional(CONF_TRACK_NEW, default=True): cv.boolean, vol.Required(CONF_CLIENT_ID): cv.string,
vol.Optional(CONF_CALENDAR_ACCESS, default="read_write"): cv.enum( vol.Required(CONF_CLIENT_SECRET): cv.string,
FeatureAccess vol.Optional(CONF_TRACK_NEW, default=True): cv.boolean,
), vol.Optional(CONF_CALENDAR_ACCESS, default="read_write"): cv.enum(
} FeatureAccess
) ),
}, }
)
},
),
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
_SINGLE_CALSEARCH_CONFIG = vol.All( _SINGLE_CALSEARCH_CONFIG = vol.All(
cv.deprecated(CONF_MAX_RESULTS), cv.deprecated(CONF_MAX_RESULTS),
cv.deprecated(CONF_TRACK),
vol.Schema( vol.Schema(
{ {
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,
@ -160,6 +164,9 @@ ADD_EVENT_SERVICE_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Google component.""" """Set up the Google component."""
if DOMAIN not in config:
return True
conf = config.get(DOMAIN, {}) conf = config.get(DOMAIN, {})
hass.data[DOMAIN] = {DATA_CONFIG: conf} hass.data[DOMAIN] = {DATA_CONFIG: conf}
@ -189,11 +196,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
}, },
) )
) )
_LOGGER.warning(
"Configuration of Google Calendar in YAML in configuration.yaml is "
"is deprecated and will be removed in a future release; Your existing "
"OAuth Application Credentials and other settings have been imported "
"into the UI automatically and can be safely removed from your "
"configuration.yaml file"
)
return True return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Google from a config entry.""" """Set up Google from a config entry."""
hass.data.setdefault(DOMAIN, {})
async_upgrade_entry(hass, entry)
implementation = ( implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation( await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry hass, entry
@ -216,8 +234,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except aiohttp.ClientError as err: except aiohttp.ClientError as err:
raise ConfigEntryNotReady from err raise ConfigEntryNotReady from err
access = get_feature_access(hass) access = FeatureAccess[entry.options[CONF_CALENDAR_ACCESS]]
if access.scope not in session.token.get("scope", []): token_scopes = session.token.get("scope", [])
if access.scope not in token_scopes:
_LOGGER.debug("Scope '%s' not in scopes '%s'", access.scope, token_scopes)
raise ConfigEntryAuthFailed( raise ConfigEntryAuthFailed(
"Required scopes are not available, reauth required" "Required scopes are not available, reauth required"
) )
@ -226,25 +246,53 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) )
hass.data[DOMAIN][DATA_SERVICE] = calendar_service hass.data[DOMAIN][DATA_SERVICE] = calendar_service
track_new = hass.data[DOMAIN][DATA_CONFIG].get(CONF_TRACK_NEW, True) await async_setup_services(hass, calendar_service)
await async_setup_services(hass, track_new, calendar_service)
# Only expose the add event service if we have the correct permissions # Only expose the add event service if we have the correct permissions
if access is FeatureAccess.read_write: if access is FeatureAccess.read_write:
await async_setup_add_event_service(hass, calendar_service) await async_setup_add_event_service(hass, calendar_service)
hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)
# Reload entry when options are updated
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
return True return True
def async_upgrade_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Upgrade the config entry if needed."""
if DATA_CONFIG not in hass.data[DOMAIN] and entry.options:
return
options = (
entry.options
if entry.options
else {
CONF_CALENDAR_ACCESS: get_feature_access(hass).name,
}
)
disable_new_entities = (
not hass.data[DOMAIN].get(DATA_CONFIG, {}).get(CONF_TRACK_NEW, True)
)
hass.config_entries.async_update_entry(
entry,
options=options,
pref_disable_new_entities=disable_new_entities,
)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload the config entry when it changed."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_setup_services( async def async_setup_services(
hass: HomeAssistant, hass: HomeAssistant,
track_new: bool,
calendar_service: GoogleCalendarService, calendar_service: GoogleCalendarService,
) -> None: ) -> None:
"""Set up the service listeners.""" """Set up the service listeners."""
@ -256,10 +304,7 @@ async def async_setup_services(
async def _found_calendar(calendar_item: Calendar) -> None: async def _found_calendar(calendar_item: Calendar) -> None:
calendar = get_calendar_info( calendar = get_calendar_info(
hass, hass,
{ calendar_item.dict(exclude_unset=True),
**calendar_item.dict(exclude_unset=True),
CONF_TRACK: track_new,
},
) )
calendar_id = calendar_item.id calendar_id = calendar_item.id
# Populate the yaml file with all discovered calendars # Populate the yaml file with all discovered calendars
@ -363,7 +408,6 @@ def get_calendar_info(
CONF_CAL_ID: calendar["id"], CONF_CAL_ID: calendar["id"],
CONF_ENTITIES: [ CONF_ENTITIES: [
{ {
CONF_TRACK: calendar["track"],
CONF_NAME: calendar["summary"], CONF_NAME: calendar["summary"],
CONF_DEVICE_ID: generate_entity_id( CONF_DEVICE_ID: generate_entity_id(
"{}", calendar["summary"], hass=hass "{}", calendar["summary"], hass=hass

View File

@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable
import datetime import datetime
import logging import logging
import time import time
from typing import Any from typing import Any, cast
import aiohttp import aiohttp
from gcal_sync.auth import AbstractAuth from gcal_sync.auth import AbstractAuth
@ -76,12 +76,12 @@ class DeviceFlow:
@property @property
def verification_url(self) -> str: def verification_url(self) -> str:
"""Return the verification url that the user should visit to enter the code.""" """Return the verification url that the user should visit to enter the code."""
return self._device_flow_info.verification_url return self._device_flow_info.verification_url # type: ignore[no-any-return]
@property @property
def user_code(self) -> str: def user_code(self) -> str:
"""Return the code that the user should enter at the verification url.""" """Return the code that the user should enter at the verification url."""
return self._device_flow_info.user_code return self._device_flow_info.user_code # type: ignore[no-any-return]
async def start_exchange_task( async def start_exchange_task(
self, finished_cb: Callable[[Credentials | None], Awaitable[None]] self, finished_cb: Callable[[Credentials | None], Awaitable[None]]
@ -131,10 +131,13 @@ def get_feature_access(hass: HomeAssistant) -> FeatureAccess:
"""Return the desired calendar feature access.""" """Return the desired calendar feature access."""
# This may be called during config entry setup without integration setup running when there # This may be called during config entry setup without integration setup running when there
# is no google entry in configuration.yaml # is no google entry in configuration.yaml
return ( return cast(
hass.data.get(DOMAIN, {}) FeatureAccess,
.get(DATA_CONFIG, {}) (
.get(CONF_CALENDAR_ACCESS, DEFAULT_FEATURE_ACCESS) hass.data.get(DOMAIN, {})
.get(DATA_CONFIG, {})
.get(CONF_CALENDAR_ACCESS, DEFAULT_FEATURE_ACCESS)
),
) )
@ -157,7 +160,7 @@ async def async_create_device_flow(
return DeviceFlow(hass, oauth_flow, device_flow_info) return DeviceFlow(hass, oauth_flow, device_flow_info)
class ApiAuthImpl(AbstractAuth): class ApiAuthImpl(AbstractAuth): # type: ignore[misc]
"""Authentication implementation for google calendar api library.""" """Authentication implementation for google calendar api library."""
def __init__( def __init__(
@ -172,10 +175,10 @@ class ApiAuthImpl(AbstractAuth):
async def async_get_access_token(self) -> str: async def async_get_access_token(self) -> str:
"""Return a valid access token.""" """Return a valid access token."""
await self._session.async_ensure_token_valid() await self._session.async_ensure_token_valid()
return self._session.token["access_token"] return cast(str, self._session.token["access_token"])
class AccessTokenAuthImpl(AbstractAuth): class AccessTokenAuthImpl(AbstractAuth): # type: ignore[misc]
"""Authentication implementation used during config flow, without refresh. """Authentication implementation used during config flow, without refresh.
This exists to allow the config flow to use the API before it has fully This exists to allow the config flow to use the API before it has fully

View File

@ -1,4 +1,5 @@
"""Support for Google Calendar Search binary sensors.""" """Support for Google Calendar Search binary sensors."""
from __future__ import annotations from __future__ import annotations
import copy import copy
@ -89,14 +90,25 @@ def _async_setup_entities(
) -> None: ) -> None:
calendar_service = hass.data[DOMAIN][DATA_SERVICE] calendar_service = hass.data[DOMAIN][DATA_SERVICE]
entities = [] entities = []
num_entities = len(disc_info[CONF_ENTITIES])
for data in disc_info[CONF_ENTITIES]: for data in disc_info[CONF_ENTITIES]:
if not data[CONF_TRACK]: entity_enabled = data.get(CONF_TRACK, True)
continue entity_name = data[CONF_DEVICE_ID]
entity_id = generate_entity_id( entity_id = generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass)
ENTITY_ID_FORMAT, data[CONF_DEVICE_ID], hass=hass calendar_id = disc_info[CONF_CAL_ID]
) if num_entities > 1:
# The google_calendars.yaml file lets users add multiple entities for
# the same calendar id and needs additional disambiguation
unique_id = f"{calendar_id}-{entity_name}"
else:
unique_id = calendar_id
entity = GoogleCalendarEntity( entity = GoogleCalendarEntity(
calendar_service, disc_info[CONF_CAL_ID], data, entity_id calendar_service,
disc_info[CONF_CAL_ID],
data,
entity_id,
unique_id,
entity_enabled,
) )
entities.append(entity) entities.append(entity)
@ -112,6 +124,8 @@ class GoogleCalendarEntity(CalendarEntity):
calendar_id: str, calendar_id: str,
data: dict[str, Any], data: dict[str, Any],
entity_id: str, entity_id: str,
unique_id: str,
entity_enabled: bool,
) -> None: ) -> None:
"""Create the Calendar event device.""" """Create the Calendar event device."""
self._calendar_service = calendar_service self._calendar_service = calendar_service
@ -123,6 +137,8 @@ class GoogleCalendarEntity(CalendarEntity):
self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET) self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET)
self._offset_value: timedelta | None = None self._offset_value: timedelta | None = None
self.entity_id = entity_id self.entity_id = entity_id
self._attr_unique_id = unique_id
self._attr_entity_registry_enabled_default = entity_enabled
@property @property
def extra_state_attributes(self) -> dict[str, bool]: def extra_state_attributes(self) -> dict[str, bool]:
@ -152,7 +168,7 @@ class GoogleCalendarEntity(CalendarEntity):
"""Return True if the event is visible.""" """Return True if the event is visible."""
if self._ignore_availability: if self._ignore_availability:
return True return True
return event.transparency == OPAQUE return event.transparency == OPAQUE # type: ignore[no-any-return]
async def async_get_events( async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime self, hass: HomeAssistant, start_date: datetime, end_date: datetime

View File

@ -7,7 +7,10 @@ from typing import Any
from gcal_sync.api import GoogleCalendarService from gcal_sync.api import GoogleCalendarService
from gcal_sync.exceptions import ApiException from gcal_sync.exceptions import ApiException
from oauth2client.client import Credentials from oauth2client.client import Credentials
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -21,7 +24,7 @@ from .api import (
async_create_device_flow, async_create_device_flow,
get_feature_access, get_feature_access,
) )
from .const import DOMAIN from .const import CONF_CALENDAR_ACCESS, DOMAIN, FeatureAccess
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -36,7 +39,7 @@ class OAuth2FlowHandler(
def __init__(self) -> None: def __init__(self) -> None:
"""Set up instance.""" """Set up instance."""
super().__init__() super().__init__()
self._reauth = False self._reauth_config_entry: config_entries.ConfigEntry | None = None
self._device_flow: DeviceFlow | None = None self._device_flow: DeviceFlow | None = None
@property @property
@ -60,7 +63,7 @@ class OAuth2FlowHandler(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
"""Handle external yaml configuration.""" """Handle external yaml configuration."""
if not self._reauth and self._async_current_entries(): if not self._reauth_config_entry and self._async_current_entries():
return self.async_abort(reason="already_configured") return self.async_abort(reason="already_configured")
return await super().async_step_user(user_input) return await super().async_step_user(user_input)
@ -84,12 +87,17 @@ class OAuth2FlowHandler(
self.flow_impl, self.flow_impl,
) )
return self.async_abort(reason="oauth_error") return self.async_abort(reason="oauth_error")
calendar_access = get_feature_access(self.hass)
if self._reauth_config_entry and self._reauth_config_entry.options:
calendar_access = FeatureAccess[
self._reauth_config_entry.options[CONF_CALENDAR_ACCESS]
]
try: try:
device_flow = await async_create_device_flow( device_flow = await async_create_device_flow(
self.hass, self.hass,
self.flow_impl.client_id, self.flow_impl.client_id,
self.flow_impl.client_secret, self.flow_impl.client_secret,
get_feature_access(self.hass), calendar_access,
) )
except OAuthError as err: except OAuthError as err:
_LOGGER.error("Error initializing device flow: %s", str(err)) _LOGGER.error("Error initializing device flow: %s", str(err))
@ -146,13 +154,21 @@ class OAuth2FlowHandler(
_LOGGER.debug("Error reading calendar primary calendar: %s", err) _LOGGER.debug("Error reading calendar primary calendar: %s", err)
primary_calendar = None primary_calendar = None
title = primary_calendar.id if primary_calendar else self.flow_impl.name title = primary_calendar.id if primary_calendar else self.flow_impl.name
return self.async_create_entry(title=title, data=data) return self.async_create_entry(
title=title,
data=data,
options={
CONF_CALENDAR_ACCESS: get_feature_access(self.hass).name,
},
)
async def async_step_reauth( async def async_step_reauth(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
"""Perform reauth upon an API authentication error.""" """Perform reauth upon an API authentication error."""
self._reauth = True self._reauth_config_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_reauth_confirm() return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm( async def async_step_reauth_confirm(
@ -162,3 +178,43 @@ class OAuth2FlowHandler(
if user_input is None: if user_input is None:
return self.async_show_form(step_id="reauth_confirm") return self.async_show_form(step_id="reauth_confirm")
return await self.async_step_user() return await self.async_step_user()
@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Create an options flow."""
return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Google Calendar options flow."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(
CONF_CALENDAR_ACCESS,
default=self.config_entry.options.get(CONF_CALENDAR_ACCESS),
): vol.In(
{
"read_write": "Read/Write access (can create events)",
"read_only": "Read-only access",
}
)
}
),
)

View File

@ -27,5 +27,14 @@
"progress": { "progress": {
"exchange": "To link your Google account, visit the [{url}]({url}) and enter code:\n\n{user_code}" "exchange": "To link your Google account, visit the [{url}]({url}) and enter code:\n\n{user_code}"
} }
},
"options": {
"step": {
"init": {
"data": {
"calendar_access": "Home Assistant access to Google Calendar"
}
}
}
} }
} }

View File

@ -27,5 +27,14 @@
"title": "Reauthenticate Integration" "title": "Reauthenticate Integration"
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"calendar_access": "Home Assistant access to Google Calendar"
}
}
}
} }
} }

View File

@ -874,6 +874,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.google.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.greeneye_monitor.*] [mypy-homeassistant.components.greeneye_monitor.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true

View File

@ -21,6 +21,7 @@ from homeassistant.components.application_credentials import (
async_import_client_credential, async_import_client_credential,
) )
from homeassistant.components.google.const import DOMAIN from homeassistant.components.google.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
@ -143,6 +144,7 @@ async def test_full_flow_yaml_creds(
"token_type": "Bearer", "token_type": "Bearer",
}, },
} }
assert result.get("options") == {"calendar_access": "read_write"}
assert len(mock_setup.mock_calls) == 1 assert len(mock_setup.mock_calls) == 1
entries = hass.config_entries.async_entries(DOMAIN) entries = hass.config_entries.async_entries(DOMAIN)
@ -205,6 +207,7 @@ async def test_full_flow_application_creds(
"token_type": "Bearer", "token_type": "Bearer",
}, },
} }
assert result.get("options") == {"calendar_access": "read_write"}
assert len(mock_setup.mock_calls) == 1 assert len(mock_setup.mock_calls) == 1
entries = hass.config_entries.async_entries(DOMAIN) entries = hass.config_entries.async_entries(DOMAIN)
@ -441,7 +444,12 @@ async def test_reauth_flow(
assert await component_setup() assert await component_setup()
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=config_entry.data DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"entry_id": config_entry.entry_id,
},
data=config_entry.data,
) )
assert result["type"] == "form" assert result["type"] == "form"
assert result["step_id"] == "reauth_confirm" assert result["step_id"] == "reauth_confirm"
@ -523,3 +531,66 @@ async def test_title_lookup_failure(
assert len(mock_setup.mock_calls) == 1 assert len(mock_setup.mock_calls) == 1
entries = hass.config_entries.async_entries(DOMAIN) entries = hass.config_entries.async_entries(DOMAIN)
assert len(entries) == 1 assert len(entries) == 1
async def test_options_flow_triggers_reauth(
hass: HomeAssistant,
component_setup: ComponentSetup,
config_entry: MockConfigEntry,
) -> None:
"""Test load and unload of a ConfigEntry."""
config_entry.add_to_hass(hass)
await component_setup()
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.options == {"calendar_access": "read_write"}
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == "form"
assert result["step_id"] == "init"
data_schema = result["data_schema"].schema
assert set(data_schema) == {"calendar_access"}
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
"calendar_access": "read_only",
},
)
assert result["type"] == "create_entry"
await hass.async_block_till_done()
assert config_entry.options == {"calendar_access": "read_only"}
# Re-auth flow was initiated because access level changed
assert config_entry.state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["step_id"] == "reauth_confirm"
async def test_options_flow_no_changes(
hass: HomeAssistant,
component_setup: ComponentSetup,
config_entry: MockConfigEntry,
) -> None:
"""Test load and unload of a ConfigEntry."""
config_entry.add_to_hass(hass)
await component_setup()
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.options == {"calendar_access": "read_write"}
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == "form"
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"],
user_input={
"calendar_access": "read_write",
},
)
assert result["type"] == "create_entry"
await hass.async_block_till_done()
assert config_entry.options == {"calendar_access": "read_write"}
# Re-auth flow was initiated because access level changed
assert config_entry.state is ConfigEntryState.LOADED

View File

@ -46,7 +46,7 @@ HassApi = Callable[[], Awaitable[dict[str, Any]]]
def assert_state(actual: State | None, expected: State | None) -> None: def assert_state(actual: State | None, expected: State | None) -> None:
"""Assert that the two states are equal.""" """Assert that the two states are equal."""
if actual is None: if actual is None or expected is None:
assert actual == expected assert actual == expected
return return
assert actual.entity_id == expected.entity_id assert actual.entity_id == expected.entity_id