mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
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:
parent
9c3f949165
commit
e6ffae8bd3
@ -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.*
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,5 +27,14 @@
|
|||||||
"title": "Reauthenticate Integration"
|
"title": "Reauthenticate Integration"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"calendar_access": "Home Assistant access to Google Calendar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
11
mypy.ini
11
mypy.ini
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user