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 collections.abc import Mapping
from dataclasses import dataclass
import dataclasses
from datetime import datetime, timedelta
import logging
from typing import Any, cast
from gcal_sync.api import Range, SyncEventsRequest
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.sync import CalendarEventSyncManager
@ -84,18 +84,19 @@ RRULE_PREFIX = "RRULE:"
SERVICE_CREATE_EVENT = "create_event"
@dataclass(frozen=True, kw_only=True)
@dataclasses.dataclass(frozen=True, kw_only=True)
class GoogleCalendarEntityDescription(CalendarEntityDescription):
"""Google calendar entity description."""
name: str
entity_id: str
name: str | None
entity_id: str | None
read_only: bool
ignore_availability: bool
offset: str | None
search: str | None
local_sync: bool
device_id: str
working_location: bool = False
def _get_entity_descriptions(
@ -142,8 +143,7 @@ def _get_entity_descriptions(
) or calendar_item.access_role == AccessRole.FREE_BUSY_READER:
read_only = True
local_sync = False
entity_descriptions.append(
GoogleCalendarEntityDescription(
entity_description = GoogleCalendarEntityDescription(
key=key,
name=data[CONF_NAME].capitalize(),
entity_id=generate_entity_id(
@ -157,6 +157,27 @@ def _get_entity_descriptions(
entity_registry_enabled_default=entity_enabled,
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
@ -233,12 +254,13 @@ async def async_setup_entry(
entity_registry.async_remove(
entity_entry.entity_id,
)
_LOGGER.debug("Creating entity with unique_id=%s", unique_id)
coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator
if not entity_description.local_sync:
coordinator = CalendarQueryUpdateCoordinator(
hass,
calendar_service,
entity_description.name,
entity_description.name or entity_description.key,
calendar_id,
entity_description.search,
)
@ -257,7 +279,7 @@ async def async_setup_entry(
coordinator = CalendarSyncUpdateCoordinator(
hass,
sync,
entity_description.name,
entity_description.name or entity_description.key,
)
entities.append(
GoogleCalendarEntity(
@ -310,11 +332,14 @@ class GoogleCalendarEntity(
) -> None:
"""Create the Calendar event device."""
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.entity_description = entity_description
self._ignore_availability = entity_description.ignore_availability
self._offset = entity_description.offset
self._event: CalendarEvent | None = None
if entity_description.entity_id:
self.entity_id = entity_description.entity_id
self._attr_unique_id = unique_id
if not entity_description.read_only:
@ -343,6 +368,8 @@ class GoogleCalendarEntity(
def _event_filter(self, event: Event) -> bool:
"""Return True if the event is visible."""
if event.event_type == EventTypeEnum.WORKING_LOCATION:
return self.entity_description.working_location
if self._ignore_availability:
return True
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"
@pytest.fixture
def calendar_is_primary() -> bool:
"""Set if the calendar is the primary or not."""
return False
@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 {
**TEST_API_CALENDAR,
"accessRole": calendar_access_role,
"primary": calendar_is_primary,
}

View File

@ -15,9 +15,11 @@ from gcal_sync.auth import API_BASE_URL
import pytest
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.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_registry import RegistryEntryDisabler
from homeassistant.helpers.template import DATE_STR_FORMAT
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["recurrence_id"] == "_c8rinwq863h45qnucyoi43ny8_20230915"
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