Remove google scan_for_calendars service and simplify platform setup (#73010)
* Remove google scan_for_calendars service and simplify platform setup * Update invalid calendar yaml testpull/73029/head
parent
b5fe4e8474
commit
9d933e732b
|
@ -1,7 +1,6 @@
|
||||||
"""Support for Google - Calendar Event Devices."""
|
"""Support for Google - Calendar Event Devices."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
|
@ -9,8 +8,7 @@ from typing import Any
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from gcal_sync.api import GoogleCalendarService
|
from gcal_sync.api import GoogleCalendarService
|
||||||
from gcal_sync.exceptions import ApiException
|
from gcal_sync.model import DateOrDatetime, Event
|
||||||
from gcal_sync.model import Calendar, DateOrDatetime, Event
|
|
||||||
from oauth2client.file import Storage
|
from oauth2client.file import Storage
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous.error import Error as VoluptuousError
|
from voluptuous.error import Error as VoluptuousError
|
||||||
|
@ -31,15 +29,10 @@ from homeassistant.const import (
|
||||||
CONF_OFFSET,
|
CONF_OFFSET,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
from homeassistant.exceptions import (
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
ConfigEntryAuthFailed,
|
|
||||||
ConfigEntryNotReady,
|
|
||||||
HomeAssistantError,
|
|
||||||
)
|
|
||||||
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
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|
||||||
from homeassistant.helpers.entity import generate_entity_id
|
from homeassistant.helpers.entity import generate_entity_id
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
@ -49,7 +42,6 @@ from .const import (
|
||||||
DATA_CONFIG,
|
DATA_CONFIG,
|
||||||
DATA_SERVICE,
|
DATA_SERVICE,
|
||||||
DEVICE_AUTH_IMPL,
|
DEVICE_AUTH_IMPL,
|
||||||
DISCOVER_CALENDAR,
|
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
FeatureAccess,
|
FeatureAccess,
|
||||||
)
|
)
|
||||||
|
@ -86,7 +78,6 @@ NOTIFICATION_ID = "google_calendar_notification"
|
||||||
NOTIFICATION_TITLE = "Google Calendar Setup"
|
NOTIFICATION_TITLE = "Google Calendar Setup"
|
||||||
GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors"
|
GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors"
|
||||||
|
|
||||||
SERVICE_SCAN_CALENDARS = "scan_for_calendars"
|
|
||||||
SERVICE_ADD_EVENT = "add_event"
|
SERVICE_ADD_EVENT = "add_event"
|
||||||
|
|
||||||
YAML_DEVICES = f"{DOMAIN}_calendars.yaml"
|
YAML_DEVICES = f"{DOMAIN}_calendars.yaml"
|
||||||
|
@ -248,7 +239,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
)
|
)
|
||||||
hass.data[DOMAIN][DATA_SERVICE] = calendar_service
|
hass.data[DOMAIN][DATA_SERVICE] = calendar_service
|
||||||
|
|
||||||
await async_setup_services(hass, 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 get_feature_access(hass, entry) is FeatureAccess.read_write:
|
if get_feature_access(hass, entry) is FeatureAccess.read_write:
|
||||||
await async_setup_add_event_service(hass, calendar_service)
|
await async_setup_add_event_service(hass, calendar_service)
|
||||||
|
@ -278,57 +268,6 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_services(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
calendar_service: GoogleCalendarService,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the service listeners."""
|
|
||||||
|
|
||||||
calendars = await hass.async_add_executor_job(
|
|
||||||
load_config, hass.config.path(YAML_DEVICES)
|
|
||||||
)
|
|
||||||
calendars_file_lock = asyncio.Lock()
|
|
||||||
|
|
||||||
async def _found_calendar(calendar_item: Calendar) -> None:
|
|
||||||
calendar = get_calendar_info(
|
|
||||||
hass,
|
|
||||||
calendar_item.dict(exclude_unset=True),
|
|
||||||
)
|
|
||||||
calendar_id = calendar_item.id
|
|
||||||
# If the google_calendars.yaml file already exists, populate it for
|
|
||||||
# backwards compatibility, but otherwise do not create it if it does
|
|
||||||
# not exist.
|
|
||||||
if calendars:
|
|
||||||
if calendar_id not in calendars:
|
|
||||||
calendars[calendar_id] = calendar
|
|
||||||
async with calendars_file_lock:
|
|
||||||
await hass.async_add_executor_job(
|
|
||||||
update_config, hass.config.path(YAML_DEVICES), calendar
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Prefer entity/name information from yaml, overriding api
|
|
||||||
calendar = calendars[calendar_id]
|
|
||||||
async_dispatcher_send(hass, DISCOVER_CALENDAR, calendar)
|
|
||||||
|
|
||||||
created_calendars = set()
|
|
||||||
|
|
||||||
async def _scan_for_calendars(call: ServiceCall) -> None:
|
|
||||||
"""Scan for new calendars."""
|
|
||||||
try:
|
|
||||||
result = await calendar_service.async_list_calendars()
|
|
||||||
except ApiException as err:
|
|
||||||
raise HomeAssistantError(str(err)) from err
|
|
||||||
tasks = []
|
|
||||||
for calendar_item in result.items:
|
|
||||||
if calendar_item.id in created_calendars:
|
|
||||||
continue
|
|
||||||
created_calendars.add(calendar_item.id)
|
|
||||||
tasks.append(_found_calendar(calendar_item))
|
|
||||||
await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
hass.services.async_register(DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_add_event_service(
|
async def async_setup_add_event_service(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
calendar_service: GoogleCalendarService,
|
calendar_service: GoogleCalendarService,
|
||||||
|
|
|
@ -20,24 +20,24 @@ from homeassistant.components.calendar import (
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET
|
from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
||||||
from homeassistant.helpers.entity import generate_entity_id
|
from homeassistant.helpers.entity import generate_entity_id
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
CONF_CAL_ID,
|
|
||||||
CONF_IGNORE_AVAILABILITY,
|
CONF_IGNORE_AVAILABILITY,
|
||||||
CONF_SEARCH,
|
CONF_SEARCH,
|
||||||
CONF_TRACK,
|
CONF_TRACK,
|
||||||
DATA_SERVICE,
|
DATA_SERVICE,
|
||||||
DEFAULT_CONF_OFFSET,
|
DEFAULT_CONF_OFFSET,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_SCAN_CALENDARS,
|
YAML_DEVICES,
|
||||||
|
get_calendar_info,
|
||||||
|
load_config,
|
||||||
|
update_config,
|
||||||
)
|
)
|
||||||
from .const import DISCOVER_CALENDAR
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -59,66 +59,63 @@ async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the google calendar platform."""
|
"""Set up the google calendar platform."""
|
||||||
|
calendar_service = hass.data[DOMAIN][DATA_SERVICE]
|
||||||
@callback
|
|
||||||
def async_discover(discovery_info: dict[str, Any]) -> None:
|
|
||||||
_async_setup_entities(
|
|
||||||
hass,
|
|
||||||
entry,
|
|
||||||
async_add_entities,
|
|
||||||
discovery_info,
|
|
||||||
)
|
|
||||||
|
|
||||||
entry.async_on_unload(
|
|
||||||
async_dispatcher_connect(hass, DISCOVER_CALENDAR, async_discover)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Look for any new calendars
|
|
||||||
try:
|
try:
|
||||||
await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, blocking=True)
|
result = await calendar_service.async_list_calendars()
|
||||||
except HomeAssistantError as err:
|
except ApiException as err:
|
||||||
# This can happen if there's a connection error during setup.
|
|
||||||
raise PlatformNotReady(str(err)) from err
|
raise PlatformNotReady(str(err)) from err
|
||||||
|
|
||||||
|
# Yaml configuration may override objects from the API
|
||||||
@callback
|
calendars = await hass.async_add_executor_job(
|
||||||
def _async_setup_entities(
|
load_config, hass.config.path(YAML_DEVICES)
|
||||||
hass: HomeAssistant,
|
)
|
||||||
entry: ConfigEntry,
|
new_calendars = []
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
disc_info: dict[str, Any],
|
|
||||||
) -> None:
|
|
||||||
calendar_service = hass.data[DOMAIN][DATA_SERVICE]
|
|
||||||
entities = []
|
entities = []
|
||||||
num_entities = len(disc_info[CONF_ENTITIES])
|
for calendar_item in result.items:
|
||||||
for data in disc_info[CONF_ENTITIES]:
|
calendar_id = calendar_item.id
|
||||||
entity_enabled = data.get(CONF_TRACK, True)
|
if calendars and calendar_id in calendars:
|
||||||
if not entity_enabled:
|
calendar_info = calendars[calendar_id]
|
||||||
_LOGGER.warning(
|
|
||||||
"The 'track' option in google_calendars.yaml has been deprecated. The setting "
|
|
||||||
"has been imported to the UI, and should now be removed from google_calendars.yaml"
|
|
||||||
)
|
|
||||||
entity_name = data[CONF_DEVICE_ID]
|
|
||||||
entity_id = generate_entity_id(ENTITY_ID_FORMAT, entity_name, 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:
|
else:
|
||||||
unique_id = calendar_id
|
calendar_info = get_calendar_info(
|
||||||
entity = GoogleCalendarEntity(
|
hass, calendar_item.dict(exclude_unset=True)
|
||||||
calendar_service,
|
)
|
||||||
disc_info[CONF_CAL_ID],
|
new_calendars.append(calendar_info)
|
||||||
data,
|
|
||||||
entity_id,
|
# Yaml calendar config may map one calendar to multiple entities with extra options like
|
||||||
unique_id,
|
# offsets or search criteria.
|
||||||
entity_enabled,
|
num_entities = len(calendar_info[CONF_ENTITIES])
|
||||||
)
|
for data in calendar_info[CONF_ENTITIES]:
|
||||||
entities.append(entity)
|
entity_enabled = data.get(CONF_TRACK, True)
|
||||||
|
if not entity_enabled:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"The 'track' option in google_calendars.yaml has been deprecated. The setting "
|
||||||
|
"has been imported to the UI, and should now be removed from google_calendars.yaml"
|
||||||
|
)
|
||||||
|
entity_name = data[CONF_DEVICE_ID]
|
||||||
|
entities.append(
|
||||||
|
GoogleCalendarEntity(
|
||||||
|
calendar_service,
|
||||||
|
calendar_id,
|
||||||
|
data,
|
||||||
|
generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass),
|
||||||
|
# The google_calendars.yaml file lets users add multiple entities for
|
||||||
|
# the same calendar id and needs additional disambiguation
|
||||||
|
f"{calendar_id}-{entity_name}" if num_entities > 1 else calendar_id,
|
||||||
|
entity_enabled,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
async_add_entities(entities, True)
|
async_add_entities(entities, True)
|
||||||
|
|
||||||
|
if calendars and new_calendars:
|
||||||
|
|
||||||
|
def append_calendars_to_config() -> None:
|
||||||
|
path = hass.config.path(YAML_DEVICES)
|
||||||
|
for calendar in new_calendars:
|
||||||
|
update_config(path, calendar)
|
||||||
|
|
||||||
|
await hass.async_add_executor_job(append_calendars_to_config)
|
||||||
|
|
||||||
|
|
||||||
class GoogleCalendarEntity(CalendarEntity):
|
class GoogleCalendarEntity(CalendarEntity):
|
||||||
"""A calendar event device."""
|
"""A calendar event device."""
|
||||||
|
|
|
@ -11,8 +11,6 @@ DATA_CALENDARS = "calendars"
|
||||||
DATA_SERVICE = "service"
|
DATA_SERVICE = "service"
|
||||||
DATA_CONFIG = "config"
|
DATA_CONFIG = "config"
|
||||||
|
|
||||||
DISCOVER_CALENDAR = "google_discover_calendar"
|
|
||||||
|
|
||||||
|
|
||||||
class FeatureAccess(Enum):
|
class FeatureAccess(Enum):
|
||||||
"""Class to represent different access scopes."""
|
"""Class to represent different access scopes."""
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
scan_for_calendars:
|
|
||||||
name: Scan for calendars
|
|
||||||
description: Scan for new calendars.
|
|
||||||
add_event:
|
add_event:
|
||||||
name: Add event
|
name: Add event
|
||||||
description: Add a new calendar event.
|
description: Add a new calendar event.
|
||||||
|
|
|
@ -14,11 +14,7 @@ from homeassistant.components.application_credentials import (
|
||||||
ClientCredential,
|
ClientCredential,
|
||||||
async_import_client_credential,
|
async_import_client_credential,
|
||||||
)
|
)
|
||||||
from homeassistant.components.google import (
|
from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT
|
||||||
DOMAIN,
|
|
||||||
SERVICE_ADD_EVENT,
|
|
||||||
SERVICE_SCAN_CALENDARS,
|
|
||||||
)
|
|
||||||
from homeassistant.components.google.const import CONF_CALENDAR_ACCESS
|
from homeassistant.components.google.const import CONF_CALENDAR_ACCESS
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.const import STATE_OFF
|
from homeassistant.const import STATE_OFF
|
||||||
|
@ -140,17 +136,24 @@ async def test_invalid_calendar_yaml(
|
||||||
component_setup: ComponentSetup,
|
component_setup: ComponentSetup,
|
||||||
calendars_config: list[dict[str, Any]],
|
calendars_config: list[dict[str, Any]],
|
||||||
mock_calendars_yaml: None,
|
mock_calendars_yaml: None,
|
||||||
|
mock_calendars_list: ApiResult,
|
||||||
|
test_api_calendar: dict[str, Any],
|
||||||
|
mock_events_list: ApiResult,
|
||||||
setup_config_entry: MockConfigEntry,
|
setup_config_entry: MockConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test setup with missing entity id fields fails to setup the config entry."""
|
"""Test setup with missing entity id fields fails to load the platform."""
|
||||||
|
mock_calendars_list({"items": [test_api_calendar]})
|
||||||
|
mock_events_list({})
|
||||||
|
|
||||||
assert await component_setup()
|
assert await component_setup()
|
||||||
|
|
||||||
entries = hass.config_entries.async_entries(DOMAIN)
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
assert len(entries) == 1
|
assert len(entries) == 1
|
||||||
entry = entries[0]
|
entry = entries[0]
|
||||||
assert entry.state is ConfigEntryState.SETUP_ERROR
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
assert not hass.states.get(TEST_YAML_ENTITY)
|
assert not hass.states.get(TEST_YAML_ENTITY)
|
||||||
|
assert not hass.states.get(TEST_API_ENTITY)
|
||||||
|
|
||||||
|
|
||||||
async def test_calendar_yaml_error(
|
async def test_calendar_yaml_error(
|
||||||
|
@ -470,57 +473,6 @@ async def test_add_event_date_time(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_scan_calendars(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
component_setup: ComponentSetup,
|
|
||||||
mock_calendars_list: ApiResult,
|
|
||||||
mock_events_list: ApiResult,
|
|
||||||
setup_config_entry: MockConfigEntry,
|
|
||||||
aioclient_mock: AiohttpClientMocker,
|
|
||||||
) -> None:
|
|
||||||
"""Test finding a calendar from the API."""
|
|
||||||
|
|
||||||
mock_calendars_list({"items": []})
|
|
||||||
assert await component_setup()
|
|
||||||
|
|
||||||
calendar_1 = {
|
|
||||||
"id": "calendar-id-1",
|
|
||||||
"summary": "Calendar 1",
|
|
||||||
}
|
|
||||||
calendar_2 = {
|
|
||||||
"id": "calendar-id-2",
|
|
||||||
"summary": "Calendar 2",
|
|
||||||
}
|
|
||||||
|
|
||||||
aioclient_mock.clear_requests()
|
|
||||||
mock_calendars_list({"items": [calendar_1]})
|
|
||||||
mock_events_list({}, calendar_id="calendar-id-1")
|
|
||||||
await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, {}, blocking=True)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
state = hass.states.get("calendar.calendar_1")
|
|
||||||
assert state
|
|
||||||
assert state.name == "Calendar 1"
|
|
||||||
assert state.state == STATE_OFF
|
|
||||||
assert not hass.states.get("calendar.calendar_2")
|
|
||||||
|
|
||||||
aioclient_mock.clear_requests()
|
|
||||||
mock_calendars_list({"items": [calendar_1, calendar_2]})
|
|
||||||
mock_events_list({}, calendar_id="calendar-id-1")
|
|
||||||
mock_events_list({}, calendar_id="calendar-id-2")
|
|
||||||
await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, {}, blocking=True)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
state = hass.states.get("calendar.calendar_1")
|
|
||||||
assert state
|
|
||||||
assert state.name == "Calendar 1"
|
|
||||||
assert state.state == STATE_OFF
|
|
||||||
state = hass.states.get("calendar.calendar_2")
|
|
||||||
assert state
|
|
||||||
assert state.name == "Calendar 2"
|
|
||||||
assert state.state == STATE_OFF
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"config_entry_token_expiry", [datetime.datetime.max.timestamp() + 1]
|
"config_entry_token_expiry", [datetime.datetime.max.timestamp() + 1]
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue