Add a working location google calendar entity (#127016)

This commit is contained in:
Allen Porter 2024-10-01 03:14:23 -07:00 committed by GitHub
parent 963b9d9a83
commit c5ebd53079
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 156 additions and 24 deletions

View File

@ -3,14 +3,14 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
from dataclasses import dataclass import dataclasses
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from typing import Any, cast from typing import Any, cast
from gcal_sync.api import Range, SyncEventsRequest from gcal_sync.api import Range, SyncEventsRequest
from gcal_sync.exceptions import ApiException from gcal_sync.exceptions import ApiException
from gcal_sync.model import AccessRole, Calendar, DateOrDatetime, Event from gcal_sync.model import AccessRole, Calendar, DateOrDatetime, Event, EventTypeEnum
from gcal_sync.store import ScopedCalendarStore from gcal_sync.store import ScopedCalendarStore
from gcal_sync.sync import CalendarEventSyncManager from gcal_sync.sync import CalendarEventSyncManager
@ -84,18 +84,19 @@ RRULE_PREFIX = "RRULE:"
SERVICE_CREATE_EVENT = "create_event" SERVICE_CREATE_EVENT = "create_event"
@dataclass(frozen=True, kw_only=True) @dataclasses.dataclass(frozen=True, kw_only=True)
class GoogleCalendarEntityDescription(CalendarEntityDescription): class GoogleCalendarEntityDescription(CalendarEntityDescription):
"""Google calendar entity description.""" """Google calendar entity description."""
name: str name: str | None
entity_id: str entity_id: str | None
read_only: bool read_only: bool
ignore_availability: bool ignore_availability: bool
offset: str | None offset: str | None
search: str | None search: str | None
local_sync: bool local_sync: bool
device_id: str device_id: str
working_location: bool = False
def _get_entity_descriptions( def _get_entity_descriptions(
@ -142,22 +143,42 @@ def _get_entity_descriptions(
) or calendar_item.access_role == AccessRole.FREE_BUSY_READER: ) or calendar_item.access_role == AccessRole.FREE_BUSY_READER:
read_only = True read_only = True
local_sync = False local_sync = False
entity_descriptions.append( entity_description = GoogleCalendarEntityDescription(
GoogleCalendarEntityDescription( key=key,
key=key, name=data[CONF_NAME].capitalize(),
name=data[CONF_NAME].capitalize(), entity_id=generate_entity_id(
entity_id=generate_entity_id( ENTITY_ID_FORMAT, data[CONF_DEVICE_ID], hass=hass
ENTITY_ID_FORMAT, data[CONF_DEVICE_ID], hass=hass ),
), read_only=read_only,
read_only=read_only, ignore_availability=data.get(CONF_IGNORE_AVAILABILITY, False),
ignore_availability=data.get(CONF_IGNORE_AVAILABILITY, False), offset=data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET),
offset=data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET), search=search,
search=search, local_sync=local_sync,
local_sync=local_sync, entity_registry_enabled_default=entity_enabled,
entity_registry_enabled_default=entity_enabled, device_id=data[CONF_DEVICE_ID],
device_id=data[CONF_DEVICE_ID],
)
) )
entity_descriptions.append(entity_description)
_LOGGER.debug(
"calendar_item.primary=%s, search=%s, calendar_item.access_role=%s - %s",
calendar_item.primary,
search,
calendar_item.access_role,
local_sync,
)
if calendar_item.primary and local_sync:
_LOGGER.debug("work location entity")
# Create an optional disabled by default entity for Work Location
entity_descriptions.append(
dataclasses.replace(
entity_description,
key=f"{key}-work-location",
translation_key="working_location",
working_location=True,
name=None,
entity_id=None,
entity_registry_enabled_default=False,
)
)
return entity_descriptions return entity_descriptions
@ -233,12 +254,13 @@ async def async_setup_entry(
entity_registry.async_remove( entity_registry.async_remove(
entity_entry.entity_id, entity_entry.entity_id,
) )
_LOGGER.debug("Creating entity with unique_id=%s", unique_id)
coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator
if not entity_description.local_sync: if not entity_description.local_sync:
coordinator = CalendarQueryUpdateCoordinator( coordinator = CalendarQueryUpdateCoordinator(
hass, hass,
calendar_service, calendar_service,
entity_description.name, entity_description.name or entity_description.key,
calendar_id, calendar_id,
entity_description.search, entity_description.search,
) )
@ -257,7 +279,7 @@ async def async_setup_entry(
coordinator = CalendarSyncUpdateCoordinator( coordinator = CalendarSyncUpdateCoordinator(
hass, hass,
sync, sync,
entity_description.name, entity_description.name or entity_description.key,
) )
entities.append( entities.append(
GoogleCalendarEntity( GoogleCalendarEntity(
@ -310,12 +332,15 @@ class GoogleCalendarEntity(
) -> None: ) -> None:
"""Create the Calendar event device.""" """Create the Calendar event device."""
super().__init__(coordinator) super().__init__(coordinator)
_LOGGER.debug("entity_description.entity_id=%s", entity_description.entity_id)
_LOGGER.debug("entity_description=%s", entity_description)
self.calendar_id = calendar_id self.calendar_id = calendar_id
self.entity_description = entity_description self.entity_description = entity_description
self._ignore_availability = entity_description.ignore_availability self._ignore_availability = entity_description.ignore_availability
self._offset = entity_description.offset self._offset = entity_description.offset
self._event: CalendarEvent | None = None self._event: CalendarEvent | None = None
self.entity_id = entity_description.entity_id if entity_description.entity_id:
self.entity_id = entity_description.entity_id
self._attr_unique_id = unique_id self._attr_unique_id = unique_id
if not entity_description.read_only: if not entity_description.read_only:
self._attr_supported_features = ( self._attr_supported_features = (
@ -343,6 +368,8 @@ class GoogleCalendarEntity(
def _event_filter(self, event: Event) -> bool: def _event_filter(self, event: Event) -> bool:
"""Return True if the event is visible.""" """Return True if the event is visible."""
if event.event_type == EventTypeEnum.WORKING_LOCATION:
return self.entity_description.working_location
if self._ignore_availability: if self._ignore_availability:
return True return True
return event.transparency == OPAQUE return event.transparency == OPAQUE

View File

@ -123,5 +123,12 @@
} }
} }
} }
},
"entity": {
"calendar": {
"working_location": {
"name": "Working location"
}
}
} }
} }

View File

@ -98,12 +98,21 @@ def calendar_access_role() -> str:
return "owner" return "owner"
@pytest.fixture
def calendar_is_primary() -> bool:
"""Set if the calendar is the primary or not."""
return False
@pytest.fixture(name="test_api_calendar") @pytest.fixture(name="test_api_calendar")
def api_calendar(calendar_access_role: str) -> dict[str, Any]: def api_calendar(
calendar_access_role: str, calendar_is_primary: bool
) -> dict[str, Any]:
"""Return a test calendar object used in API responses.""" """Return a test calendar object used in API responses."""
return { return {
**TEST_API_CALENDAR, **TEST_API_CALENDAR,
"accessRole": calendar_access_role, "accessRole": calendar_access_role,
"primary": calendar_is_primary,
} }

View File

@ -15,9 +15,11 @@ from gcal_sync.auth import API_BASE_URL
import pytest import pytest
from homeassistant.components.google.const import CONF_CALENDAR_ACCESS, DOMAIN from homeassistant.components.google.const import CONF_CALENDAR_ACCESS, DOMAIN
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.const import STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_registry import RegistryEntryDisabler
from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.helpers.template import DATE_STR_FORMAT
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -1359,3 +1361,90 @@ async def test_invalid_rrule_fix(
assert event["uid"] == "cydrevtfuybguinhomj@google.com" assert event["uid"] == "cydrevtfuybguinhomj@google.com"
assert event["recurrence_id"] == "_c8rinwq863h45qnucyoi43ny8_20230915" assert event["recurrence_id"] == "_c8rinwq863h45qnucyoi43ny8_20230915"
assert event["rrule"] is None assert event["rrule"] is None
@pytest.mark.parametrize(
("event_type", "expected_event_message"),
[
("default", "Test All Day Event"),
("workingLocation", None),
],
)
async def test_working_location_ignored(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_events_list_items: Callable[[list[dict[str, Any]]], None],
component_setup: ComponentSetup,
event_type: str,
expected_event_message: str | None,
) -> None:
"""Test working location events are skipped."""
event = {
**TEST_EVENT,
**upcoming(),
"eventType": event_type,
}
mock_events_list_items([event])
assert await component_setup()
state = hass.states.get(TEST_ENTITY)
assert state
assert state.name == TEST_ENTITY_NAME
assert state.attributes.get("message") == expected_event_message
@pytest.mark.parametrize("calendar_is_primary", [True])
async def test_working_location_entity(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
entity_registry: er.EntityRegistry,
mock_events_list_items: Callable[[list[dict[str, Any]]], None],
component_setup: ComponentSetup,
) -> None:
"""Test that working location events are registered under a disabled by default entity."""
event = {
**TEST_EVENT,
**upcoming(),
"eventType": "workingLocation",
}
mock_events_list_items([event])
assert await component_setup()
entity_entry = entity_registry.async_get("calendar.working_location")
assert entity_entry
assert entity_entry.disabled_by == RegistryEntryDisabler.INTEGRATION
entity_registry.async_update_entity(
entity_id="calendar.working_location", disabled_by=None
)
async_fire_time_changed(
hass,
dt_util.utcnow() + datetime.timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
)
await hass.async_block_till_done()
state = hass.states.get("calendar.working_location")
assert state
assert state.name == "Working location"
assert state.attributes.get("message") == "Test All Day Event"
@pytest.mark.parametrize("calendar_is_primary", [False])
async def test_no_working_location_entity(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
entity_registry: er.EntityRegistry,
mock_events_list_items: Callable[[list[dict[str, Any]]], None],
component_setup: ComponentSetup,
) -> None:
"""Test that working location events are not registered for a secondary calendar."""
event = {
**TEST_EVENT,
**upcoming(),
"eventType": "workingLocation",
}
mock_events_list_items([event])
assert await component_setup()
entity_entry = entity_registry.async_get("calendar.working_location")
assert not entity_entry