diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 2a40bfe7043..1ddb44e570b 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -1,7 +1,6 @@ """Support for Google - Calendar Event Devices.""" from __future__ import annotations -import asyncio from collections.abc import Mapping from datetime import datetime, timedelta import logging @@ -9,8 +8,7 @@ from typing import Any import aiohttp from gcal_sync.api import GoogleCalendarService -from gcal_sync.exceptions import ApiException -from gcal_sync.model import Calendar, DateOrDatetime, Event +from gcal_sync.model import DateOrDatetime, Event from oauth2client.file import Storage import voluptuous as vol from voluptuous.error import Error as VoluptuousError @@ -31,15 +29,10 @@ from homeassistant.const import ( CONF_OFFSET, ) from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ( - ConfigEntryAuthFailed, - ConfigEntryNotReady, - HomeAssistantError, -) +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession 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.typing import ConfigType @@ -49,7 +42,6 @@ from .const import ( DATA_CONFIG, DATA_SERVICE, DEVICE_AUTH_IMPL, - DISCOVER_CALENDAR, DOMAIN, FeatureAccess, ) @@ -86,7 +78,6 @@ NOTIFICATION_ID = "google_calendar_notification" NOTIFICATION_TITLE = "Google Calendar Setup" GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors" -SERVICE_SCAN_CALENDARS = "scan_for_calendars" SERVICE_ADD_EVENT = "add_event" 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 - await async_setup_services(hass, calendar_service) # Only expose the add event service if we have the correct permissions if get_feature_access(hass, entry) is FeatureAccess.read_write: 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) -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( hass: HomeAssistant, calendar_service: GoogleCalendarService, diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index ba4368fefae..78661ed792f 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -20,24 +20,24 @@ from homeassistant.components.calendar import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET -from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError, PlatformNotReady -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import Throttle from . import ( - CONF_CAL_ID, CONF_IGNORE_AVAILABILITY, CONF_SEARCH, CONF_TRACK, DATA_SERVICE, DEFAULT_CONF_OFFSET, DOMAIN, - SERVICE_SCAN_CALENDARS, + YAML_DEVICES, + get_calendar_info, + load_config, + update_config, ) -from .const import DISCOVER_CALENDAR _LOGGER = logging.getLogger(__name__) @@ -59,66 +59,63 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the google calendar platform.""" - - @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 + calendar_service = hass.data[DOMAIN][DATA_SERVICE] try: - await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, blocking=True) - except HomeAssistantError as err: - # This can happen if there's a connection error during setup. + result = await calendar_service.async_list_calendars() + except ApiException as err: raise PlatformNotReady(str(err)) from err - -@callback -def _async_setup_entities( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, - disc_info: dict[str, Any], -) -> None: - calendar_service = hass.data[DOMAIN][DATA_SERVICE] + # Yaml configuration may override objects from the API + calendars = await hass.async_add_executor_job( + load_config, hass.config.path(YAML_DEVICES) + ) + new_calendars = [] entities = [] - num_entities = len(disc_info[CONF_ENTITIES]) - for data in disc_info[CONF_ENTITIES]: - 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] - 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}" + for calendar_item in result.items: + calendar_id = calendar_item.id + if calendars and calendar_id in calendars: + calendar_info = calendars[calendar_id] else: - unique_id = calendar_id - entity = GoogleCalendarEntity( - calendar_service, - disc_info[CONF_CAL_ID], - data, - entity_id, - unique_id, - entity_enabled, - ) - entities.append(entity) + calendar_info = get_calendar_info( + hass, calendar_item.dict(exclude_unset=True) + ) + new_calendars.append(calendar_info) + + # Yaml calendar config may map one calendar to multiple entities with extra options like + # offsets or search criteria. + num_entities = len(calendar_info[CONF_ENTITIES]) + for data in calendar_info[CONF_ENTITIES]: + 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) + 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): """A calendar event device.""" diff --git a/homeassistant/components/google/const.py b/homeassistant/components/google/const.py index c01ff1ea48b..fba9b01b600 100644 --- a/homeassistant/components/google/const.py +++ b/homeassistant/components/google/const.py @@ -11,8 +11,6 @@ DATA_CALENDARS = "calendars" DATA_SERVICE = "service" DATA_CONFIG = "config" -DISCOVER_CALENDAR = "google_discover_calendar" - class FeatureAccess(Enum): """Class to represent different access scopes.""" diff --git a/homeassistant/components/google/services.yaml b/homeassistant/components/google/services.yaml index 21df763374f..baa069aaedf 100644 --- a/homeassistant/components/google/services.yaml +++ b/homeassistant/components/google/services.yaml @@ -1,6 +1,3 @@ -scan_for_calendars: - name: Scan for calendars - description: Scan for new calendars. add_event: name: Add event description: Add a new calendar event. diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index f2cf067f7bb..cadb444c26f 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -14,11 +14,7 @@ from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, ) -from homeassistant.components.google import ( - DOMAIN, - SERVICE_ADD_EVENT, - SERVICE_SCAN_CALENDARS, -) +from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT from homeassistant.components.google.const import CONF_CALENDAR_ACCESS from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF @@ -140,17 +136,24 @@ async def test_invalid_calendar_yaml( component_setup: ComponentSetup, calendars_config: list[dict[str, Any]], mock_calendars_yaml: None, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_events_list: ApiResult, setup_config_entry: MockConfigEntry, ) -> 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() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 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_API_ENTITY) 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( "config_entry_token_expiry", [datetime.datetime.max.timestamp() + 1] )