Skip to content

Commit

Permalink
Add a working location google calendar entity (#127016)
Browse files Browse the repository at this point in the history
  • Loading branch information
allenporter authored Oct 1, 2024
1 parent 963b9d9 commit c5ebd53
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 24 deletions.
73 changes: 50 additions & 23 deletions homeassistant/components/google/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -84,18 +84,19 @@
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(
Expand Down Expand Up @@ -142,22 +143,42 @@ def _get_entity_descriptions(
) or calendar_item.access_role == AccessRole.FREE_BUSY_READER:
read_only = True
local_sync = False
entity_descriptions.append(
GoogleCalendarEntityDescription(
key=key,
name=data[CONF_NAME].capitalize(),
entity_id=generate_entity_id(
ENTITY_ID_FORMAT, data[CONF_DEVICE_ID], hass=hass
),
read_only=read_only,
ignore_availability=data.get(CONF_IGNORE_AVAILABILITY, False),
offset=data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET),
search=search,
local_sync=local_sync,
entity_registry_enabled_default=entity_enabled,
device_id=data[CONF_DEVICE_ID],
)
entity_description = GoogleCalendarEntityDescription(
key=key,
name=data[CONF_NAME].capitalize(),
entity_id=generate_entity_id(
ENTITY_ID_FORMAT, data[CONF_DEVICE_ID], hass=hass
),
read_only=read_only,
ignore_availability=data.get(CONF_IGNORE_AVAILABILITY, False),
offset=data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET),
search=search,
local_sync=local_sync,
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


Expand Down Expand Up @@ -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,
)
Expand All @@ -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(
Expand Down Expand Up @@ -310,12 +332,15 @@ def __init__(
) -> 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
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
if not entity_description.read_only:
self._attr_supported_features = (
Expand Down Expand Up @@ -343,6 +368,8 @@ def event(self) -> CalendarEvent | None:

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
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/google/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,12 @@
}
}
}
},
"entity": {
"calendar": {
"working_location": {
"name": "Working location"
}
}
}
}
11 changes: 10 additions & 1 deletion tests/components/google/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}


Expand Down
89 changes: 89 additions & 0 deletions tests/components/google/test_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
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

Expand Down Expand Up @@ -1359,3 +1361,90 @@ async def test_invalid_rrule_fix(
assert event["uid"] == "[email protected]"
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

0 comments on commit c5ebd53

Please sign in to comment.