From 6e447461097d8d9ca159a08a787bde7d66c10cba Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 6 Aug 2023 21:13:16 +0000 Subject: [PATCH 1/6] Update calendar handle state updates at start/end of active/upcoming event --- homeassistant/components/calendar/__init__.py | 48 ++++++++++- homeassistant/components/google/calendar.py | 83 +++++++------------ tests/components/local_calendar/conftest.py | 47 ++++++++++- .../local_calendar/test_calendar.py | 3 + .../local_calendar/test_diagnostics.py | 16 +--- 5 files changed, 123 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index c85f0d2bff1d0..1a1c8517ca164 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -20,6 +20,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import ( + CALLBACK_TYPE, HomeAssistant, ServiceCall, ServiceResponse, @@ -34,6 +35,7 @@ ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.event import async_track_point_in_time from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util @@ -478,6 +480,8 @@ def is_offset_reached( class CalendarEntity(Entity): """Base class for calendar event entities.""" + _alarm_unsubs: list[CALLBACK_TYPE] = [] + @property def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" @@ -503,6 +507,7 @@ def state_attributes(self) -> dict[str, Any] | None: @property def state(self) -> str: """Return the state of the calendar event.""" + _LOGGER.debug("state property called: %s", self.event) if (event := self.event) is None: return STATE_OFF @@ -513,6 +518,47 @@ def state(self) -> str: return STATE_OFF + async def async_update_ha_state(self, force_refresh: bool = False) -> None: + """Update Home Assistant with current state of entity. + + This sets up listeners to handle state transitions for start or end of + the current or upcoming event. + """ + await super().async_update_ha_state(force_refresh) + + for unsub in self._alarm_unsubs: + unsub() + + now = dt_util.now() + event = self.event + if event is None or now >= event.end_datetime_local: + return + if now < event.start_datetime_local: + self._alarm_unsubs.append( + async_track_point_in_time( + self.hass, + self._event_start_or_end_trigger, + event.start_datetime_local, + ) + ) + self._alarm_unsubs.append( + async_track_point_in_time( + self.hass, self._event_start_or_end_trigger, event.end_datetime_local + ) + ) + + async def _event_start_or_end_trigger(self, _: datetime.datetime) -> None: + """Run when the active or upcoming event starts or ends.""" + self._async_write_ha_state() + + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass. + + To be extended by integrations. + """ + for unsub in self._alarm_unsubs: + unsub() + async def async_get_events( self, hass: HomeAssistant, @@ -646,7 +692,7 @@ async def handle_calendar_event_create( ) ) return - + _LOGGER.debug("async_create_event: %s", msg[CONF_EVENT]) try: await entity.async_create_event(**msg[CONF_EVENT]) except HomeAssistantError as ex: diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 347e84449469f..ec01d82d662b9 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -36,7 +36,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import entity_platform, entity_registry as er from homeassistant.helpers.entity import generate_entity_id @@ -383,7 +383,6 @@ def __init__( self._event: CalendarEvent | None = None self._attr_name = data[CONF_NAME].capitalize() self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET) - self._offset_value: timedelta | None = None self.entity_id = entity_id self._attr_unique_id = unique_id self._attr_entity_registry_enabled_default = entity_enabled @@ -392,17 +391,6 @@ def __init__( CalendarEntityFeature.CREATE_EVENT | CalendarEntityFeature.DELETE_EVENT ) - @property - def should_poll(self) -> bool: - """Enable polling for the entity. - - The coordinator is not used by multiple entities, but instead - is used to poll the calendar API at a separate interval from the - entity state updates itself which happen more frequently (e.g. to - fire an alarm when the next event starts). - """ - return True - @property def extra_state_attributes(self) -> dict[str, bool]: """Return the device state attributes.""" @@ -411,16 +399,35 @@ def extra_state_attributes(self) -> dict[str, bool]: @property def offset_reached(self) -> bool: """Return whether or not the event offset was reached.""" - if self._event and self._offset_value: - return is_offset_reached( - self._event.start_datetime_local, self._offset_value - ) + (event, offset_value) = self._event_with_offset() + if event is not None and offset_value is not None: + return is_offset_reached(event.start_datetime_local, offset_value) return False @property def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" - return self._event + (event, _) = self._event_with_offset() + return event + + def _event_with_offset( + self, + ) -> tuple[CalendarEvent | None, timedelta | None]: + """Get the calendar event and offset if any.""" + if api_event := next( + filter( + self._event_filter, + self.coordinator.upcoming or [], + ), + None, + ): + event = _get_calendar_event(api_event) + if self._offset: + (event.summary, offset_value) = extract_offset( + event.summary, self._offset + ) + return event, offset_value + return None, None def _event_filter(self, event: Event) -> bool: """Return True if the event is visible.""" @@ -431,16 +438,13 @@ def _event_filter(self, event: Event) -> bool: async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() - # We do not ask for an update with async_add_entities() # because it will update disabled entities. This is started as a # task to let if sync in the background without blocking startup - async def refresh() -> None: - await self.coordinator.async_request_refresh() - self._apply_coordinator_update() - self.coordinator.config_entry.async_create_background_task( - self.hass, refresh(), "google.calendar-refresh" + self.hass, + self.coordinator.async_request_refresh(), + "google.calendar-refresh", ) async def async_get_events( @@ -453,37 +457,6 @@ async def async_get_events( for event in filter(self._event_filter, result_items) ] - def _apply_coordinator_update(self) -> None: - """Copy state from the coordinator to this entity.""" - if api_event := next( - filter( - self._event_filter, - self.coordinator.upcoming or [], - ), - None, - ): - self._event = _get_calendar_event(api_event) - (self._event.summary, self._offset_value) = extract_offset( - self._event.summary, self._offset - ) - else: - self._event = None - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._apply_coordinator_update() - super()._handle_coordinator_update() - - async def async_update(self) -> None: - """Disable update behavior. - - This relies on the coordinator callback update to write home assistant - state with the next calendar event. This update is a no-op as no new data - fetch is needed to evaluate the state to determine if the next event has - started, handled by CalendarEntity parent class. - """ - async def async_create_event(self, **kwargs: Any) -> None: """Add a new event to calendar.""" dtstart = kwargs[EVENT_START] diff --git a/tests/components/local_calendar/conftest.py b/tests/components/local_calendar/conftest.py index 7dc294087bd18..772dd37818160 100644 --- a/tests/components/local_calendar/conftest.py +++ b/tests/components/local_calendar/conftest.py @@ -10,12 +10,13 @@ from aiohttp import ClientWebSocketResponse import pytest +from homeassistant.auth.models import Credentials from homeassistant.components.local_calendar import LocalCalendarStore from homeassistant.components.local_calendar.const import CONF_CALENDAR_NAME, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry +from tests.common import CLIENT_ID, MockConfigEntry, MockUser from tests.typing import ClientSessionGenerator, WebSocketGenerator CALENDAR_NAME = "Light Schedule" @@ -85,14 +86,33 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry(domain=DOMAIN, data={CONF_CALENDAR_NAME: CALENDAR_NAME}) -@pytest.fixture(name="setup_integration") -async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: +async def _setup_integration( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: """Set up the integration.""" config_entry.add_to_hass(hass) assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() +@pytest.fixture(name="setup_integration") +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Set up the integration.""" + await _setup_integration(hass, config_entry) + + +@pytest.fixture(name="setup_integration_func") +async def setup_integration_func( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> Callable[[], Awaitable[None]]: + """Fixture that will set up the integration.""" + + async def _func(): + await _setup_integration(hass, config_entry) + + return _func + + GetEventsFn = Callable[[str, str], Awaitable[list[dict[str, Any]]]] @@ -154,18 +174,37 @@ async def cmd_result( return resp.get("result") +async def generate_new_hass_access_token( + hass: HomeAssistant, + hass_admin_user: MockUser, + hass_admin_credential: Credentials, +) -> str: + """Return an access token to access Home Assistant.""" + await hass.auth.async_link_user(hass_admin_user, hass_admin_credential) + + refresh_token = await hass.auth.async_create_refresh_token( + hass_admin_user, CLIENT_ID, credential=hass_admin_credential + ) + return hass.auth.async_create_access_token(refresh_token) + + ClientFixture = Callable[[], Awaitable[Client]] @pytest.fixture async def ws_client( hass: HomeAssistant, + hass_admin_user: MockUser, + hass_admin_credential: Credentials, hass_ws_client: WebSocketGenerator, ) -> ClientFixture: """Fixture for creating the test websocket client.""" async def create_client() -> Client: - ws_client = await hass_ws_client(hass) + access_token = await generate_new_hass_access_token( + hass, hass_admin_user, hass_admin_credential + ) + ws_client = await hass_ws_client(hass, access_token=access_token) return Client(ws_client) return create_client diff --git a/tests/components/local_calendar/test_calendar.py b/tests/components/local_calendar/test_calendar.py index 559a2af38b325..685623d193e31 100644 --- a/tests/components/local_calendar/test_calendar.py +++ b/tests/components/local_calendar/test_calendar.py @@ -1,6 +1,7 @@ """Tests for calendar platform of local calendar.""" import datetime +import logging import textwrap import pytest @@ -20,6 +21,8 @@ from tests.common import MockConfigEntry +_LOGGER = logging.getLogger(__name__) + async def test_empty_calendar( hass: HomeAssistant, setup_integration: None, get_events: GetEventsFn diff --git a/tests/components/local_calendar/test_diagnostics.py b/tests/components/local_calendar/test_diagnostics.py index 9a1da25d77014..6a2696cafe840 100644 --- a/tests/components/local_calendar/test_diagnostics.py +++ b/tests/components/local_calendar/test_diagnostics.py @@ -8,25 +8,13 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .conftest import TEST_ENTITY, Client +from .conftest import TEST_ENTITY, Client, generate_new_hass_access_token -from tests.common import CLIENT_ID, MockConfigEntry, MockUser +from tests.common import MockConfigEntry, MockUser from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator, WebSocketGenerator -async def generate_new_hass_access_token( - hass: HomeAssistant, hass_admin_user: MockUser, hass_admin_credential: Credentials -) -> str: - """Return an access token to access Home Assistant.""" - await hass.auth.async_link_user(hass_admin_user, hass_admin_credential) - - refresh_token = await hass.auth.async_create_refresh_token( - hass_admin_user, CLIENT_ID, credential=hass_admin_credential - ) - return hass.auth.async_create_access_token(refresh_token) - - def _get_test_client_generator( hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, new_token: str ): From ece5a0b5dd061ed008d9ddfb791f23ce902f87e8 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 8 Aug 2023 14:26:59 +0000 Subject: [PATCH 2/6] Use async_write_ha_state intercept state updates Remove unrelated changes and whitespace. --- homeassistant/components/calendar/__init__.py | 9 +- homeassistant/components/google/calendar.py | 39 +++---- homeassistant/components/nest/event.py | 106 ++++++++++++++++++ tests/components/local_calendar/conftest.py | 47 +------- .../local_calendar/test_calendar.py | 3 - .../local_calendar/test_diagnostics.py | 16 ++- 6 files changed, 149 insertions(+), 71 deletions(-) create mode 100644 homeassistant/components/nest/event.py diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 1a1c8517ca164..b81c34f272b00 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -25,6 +25,7 @@ ServiceCall, ServiceResponse, SupportsResponse, + callback, ) from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -518,13 +519,14 @@ def state(self) -> str: return STATE_OFF - async def async_update_ha_state(self, force_refresh: bool = False) -> None: - """Update Home Assistant with current state of entity. + @callback + def async_write_ha_state(self) -> None: + """Write the state to the state machine. This sets up listeners to handle state transitions for start or end of the current or upcoming event. """ - await super().async_update_ha_state(force_refresh) + super().async_write_ha_state() for unsub in self._alarm_unsubs: unsub() @@ -692,7 +694,6 @@ async def handle_calendar_event_create( ) ) return - _LOGGER.debug("async_create_event: %s", msg[CONF_EVENT]) try: await entity.async_create_event(**msg[CONF_EVENT]) except HomeAssistantError as ex: diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index ec01d82d662b9..9559a06d49c4a 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -410,25 +410,6 @@ def event(self) -> CalendarEvent | None: (event, _) = self._event_with_offset() return event - def _event_with_offset( - self, - ) -> tuple[CalendarEvent | None, timedelta | None]: - """Get the calendar event and offset if any.""" - if api_event := next( - filter( - self._event_filter, - self.coordinator.upcoming or [], - ), - None, - ): - event = _get_calendar_event(api_event) - if self._offset: - (event.summary, offset_value) = extract_offset( - event.summary, self._offset - ) - return event, offset_value - return None, None - def _event_filter(self, event: Event) -> bool: """Return True if the event is visible.""" if self._ignore_availability: @@ -438,6 +419,7 @@ def _event_filter(self, event: Event) -> bool: async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() + # We do not ask for an update with async_add_entities() # because it will update disabled entities. This is started as a # task to let if sync in the background without blocking startup @@ -457,6 +439,25 @@ async def async_get_events( for event in filter(self._event_filter, result_items) ] + def _event_with_offset( + self, + ) -> tuple[CalendarEvent | None, timedelta | None]: + """Get the calendar event and offset if any.""" + if api_event := next( + filter( + self._event_filter, + self.coordinator.upcoming or [], + ), + None, + ): + event = _get_calendar_event(api_event) + if self._offset: + (event.summary, offset_value) = extract_offset( + event.summary, self._offset + ) + return event, offset_value + return None, None + async def async_create_event(self, **kwargs: Any) -> None: """Add a new event to calendar.""" dtstart = kwargs[EVENT_START] diff --git a/homeassistant/components/nest/event.py b/homeassistant/components/nest/event.py new file mode 100644 index 0000000000000..78025dc22b4ed --- /dev/null +++ b/homeassistant/components/nest/event.py @@ -0,0 +1,106 @@ +"""Support for Google Nest Events for Cameras.""" + +import logging + +from google_nest_sdm.camera_traits import ( + CameraMotionTrait, + CameraPersonTrait, + CameraSoundTrait, +) +from google_nest_sdm.device import Device +from google_nest_sdm.device_manager import DeviceManager +from google_nest_sdm.doorbell_traits import DoorbellChimeTrait +from google_nest_sdm.event import EventMessage + +from homeassistant.components.event import ( + EventDeviceClass, + EventEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_DEVICE_MANAGER, DOMAIN +from .device_info import NestDeviceInfo + +_LOGGER = logging.getLogger(__name__) + +DOORBELL_EVENT_TRAINTS = (DoorbellChimeTrait.NAME,) +MOTION_EVENT_TRAINTS = (CameraMotionTrait, CameraPersonTrait, CameraSoundTrait) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the cameras.""" + + device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ + DATA_DEVICE_MANAGER + ] + entities = [] + for device in device_manager.devices.values(): + if any(trait in device.traits for trait in DOORBELL_EVENT_TRAINTS): + entities.append(DoorbellEvent(device)) + + async_add_entities(entities) + + +class DoorbellEvent(EventEntity): + """Entity for handling doorbell press events.""" + + def __init__(self, device: Device) -> None: + """Initialize DoorbellEvent.""" + self._device = device + self._device_info = NestDeviceInfo(device) + self._attr_has_entity_name = True + self._attr_translation_key = "doorbell" + self._attr_unique_id = f"{device.name}-{self.device_class}" + self._attr_device_info = self._device_info.device_info + self._attr_device_class = EventDeviceClass.DOORBELL + self._attr_event_types = ["press"] + + async def _async_handle_event(self, event_message: EventMessage) -> None: + """Handle a doorbell press event.""" + if not event_message.resource_update_events: + return + if DoorbellChimeTrait.EVENT_NAME in event_message.resource_update_events: + self._trigger_event("press") + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register callbacks with your device API/library.""" + self._device.add_event_callback(self._async_handle_event) + + +# 2023-08-06 23:32:04.565 DEBUG (MainThread) [google_nest_sdm.event] EventMessage raw_data={ +# 'eventId': '86539e37-1beb-4d75-83f8-a38612c20fd7', 'timestamp': '2023-08-06T23:32:01.727Z', +# 'resourceUpdate': { +# 'name': 'enterprises/9e664211-78d7-4e70-936c-a01f14179e36/devices/AVPHwEtV3Lm25VPuln9abzjLCJx6fBD-_dK1FWFYVTbvlQKWi8lBuCkapDdT1HmBZb89OR0HK7oaBfSgoPg7rkXMn3x9nA', +# 'events': { +# 'sdm.devices.events.DoorbellChime.Chime': { +# 'eventSessionId': '1632710533', 'eventId': 'n:2' +# }, +# 'sdm.devices.events.CameraClipPreview.ClipPreview': { +# 'eventSessionId': '1632710533', +# 'previewUrl': 'https://nest-camera-frontend.googleapis.com/frontend/encrypted/clippreview/AXG5WmjWL4kdZadtV-PksmouP-2PUTk8jwg0TOJrnB6YR4cXqC2I1Feq5bOdAQsOJ_BjCi0MD0Wv1OAQS1jdAuQhZ7Rio9ufEGpaqju_4ZDO5KzhCO5u-HJbXh7LXNb5dXVKeXGPuUAXDwlY5wc7ufddlN4cEUin5PntscIbVkUXl2YVKt84OnTfTu6ScF4dvzS6zptF9ghGd3rg0gHWGarZtia-WZr-PYFZhlZCJOzb3fe7V0wuZjPTx2TCNI_mA9ItF5GozTH4BnUW6nCpYVMf7yvFgqeScqIRCKcQ0yiCa5gkMcdImQ=='}}}, +# 'userId': 'AVPHwEvuuYrColDDTr3e1FU4cTrF8bdIA8O9JmZ4aUq8', +# 'eventThreadId': '21f8e530-c176-4eb5-ab3f-e574d4c224ec', +# 'resourceGroup': ['enterprises/9e664211-78d7-4e70-936c-a01f14179e36/devices/AVPHwEtV3Lm25VPuln9abzjLCJx6fBD-_dK1FWFYVTbvlQKWi8lBuCkapDdT1HmBZb89OR0HK7oaBfSgoPg7rkXMn3x9nA'], +# 'eventThreadState': 'STARTED' +# } + +# 2023-08-06 23:32:24.578 DEBUG (MainThread) [google_nest_sdm.event] EventMessage raw_data={ +# 'eventId': '86539e37-1beb-4d75-83f8-a38612c20fd7', 'timestamp': '2023-08-06T23:32:01.727Z', +# 'resourceUpdate': { +# 'name': 'enterprises/9e664211-78d7-4e70-936c-a01f14179e36/devices/AVPHwEtV3Lm25VPuln9abzjLCJx6fBD-_dK1FWFYVTbvlQKWi8lBuCkapDdT1HmBZb89OR0HK7oaBfSgoPg7rkXMn3x9nA', +# 'events': { +# 'sdm.devices.events.DoorbellChime.Chime': { +# 'eventSessionId': '1632710533', 'eventId': 'n:2' +# }, +# 'sdm.devices.events.CameraClipPreview.ClipPreview': { +# 'eventSessionId': '1632710533', 'previewUrl': 'https://nest-camera-frontend.googleapis.com/frontend/encrypted/clippreview/AXG5WmjWL4kdZadtV-PksmouP-2PUTk8jwg0TOJrnB6YR4cXqC2I1Feq5bOdAQsOJ_BjCi0MD0Wv1OAQS1jdAuQhZ7Rio9ufEGpaqju_4ZDO5KzhCO5u-HJbXh7LXNb5dXVKeXGPuUAXDwlY5wc7ufddlN4cEUin5PntscIbVkUXl2YVKt84OnTfTu6ScF4dvzS6zptF9ghGd3rg0gHWGarZtia-WZr-PYFZhlZCJOzb3fe7V0wuZjPTx2TCNI_mA9ItF5GozTH4BnUW6nCpYVMf7yvFgqeScqIRCKcQ0yiCa5gkMcdImQ=='}}}, +# 'userId': 'AVPHwEvuuYrColDDTr3e1FU4cTrF8bdIA8O9JmZ4aUq8', +# 'eventThreadId': '21f8e530-c176-4eb5-ab3f-e574d4c224ec', +# 'resourceGroup': ['enterprises/9e664211-78d7-4e70-936c-a01f14179e36/devices/AVPHwEtV3Lm25VPuln9abzjLCJx6fBD-_dK1FWFYVTbvlQKWi8lBuCkapDdT1HmBZb89OR0HK7oaBfSgoPg7rkXMn3x9nA'], +# 'eventThreadState': 'ENDED' +# } diff --git a/tests/components/local_calendar/conftest.py b/tests/components/local_calendar/conftest.py index 772dd37818160..7dc294087bd18 100644 --- a/tests/components/local_calendar/conftest.py +++ b/tests/components/local_calendar/conftest.py @@ -10,13 +10,12 @@ from aiohttp import ClientWebSocketResponse import pytest -from homeassistant.auth.models import Credentials from homeassistant.components.local_calendar import LocalCalendarStore from homeassistant.components.local_calendar.const import CONF_CALENDAR_NAME, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import CLIENT_ID, MockConfigEntry, MockUser +from tests.common import MockConfigEntry from tests.typing import ClientSessionGenerator, WebSocketGenerator CALENDAR_NAME = "Light Schedule" @@ -86,33 +85,14 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry(domain=DOMAIN, data={CONF_CALENDAR_NAME: CALENDAR_NAME}) -async def _setup_integration( - hass: HomeAssistant, config_entry: MockConfigEntry -) -> None: +@pytest.fixture(name="setup_integration") +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: """Set up the integration.""" config_entry.add_to_hass(hass) assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() -@pytest.fixture(name="setup_integration") -async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: - """Set up the integration.""" - await _setup_integration(hass, config_entry) - - -@pytest.fixture(name="setup_integration_func") -async def setup_integration_func( - hass: HomeAssistant, config_entry: MockConfigEntry -) -> Callable[[], Awaitable[None]]: - """Fixture that will set up the integration.""" - - async def _func(): - await _setup_integration(hass, config_entry) - - return _func - - GetEventsFn = Callable[[str, str], Awaitable[list[dict[str, Any]]]] @@ -174,37 +154,18 @@ async def cmd_result( return resp.get("result") -async def generate_new_hass_access_token( - hass: HomeAssistant, - hass_admin_user: MockUser, - hass_admin_credential: Credentials, -) -> str: - """Return an access token to access Home Assistant.""" - await hass.auth.async_link_user(hass_admin_user, hass_admin_credential) - - refresh_token = await hass.auth.async_create_refresh_token( - hass_admin_user, CLIENT_ID, credential=hass_admin_credential - ) - return hass.auth.async_create_access_token(refresh_token) - - ClientFixture = Callable[[], Awaitable[Client]] @pytest.fixture async def ws_client( hass: HomeAssistant, - hass_admin_user: MockUser, - hass_admin_credential: Credentials, hass_ws_client: WebSocketGenerator, ) -> ClientFixture: """Fixture for creating the test websocket client.""" async def create_client() -> Client: - access_token = await generate_new_hass_access_token( - hass, hass_admin_user, hass_admin_credential - ) - ws_client = await hass_ws_client(hass, access_token=access_token) + ws_client = await hass_ws_client(hass) return Client(ws_client) return create_client diff --git a/tests/components/local_calendar/test_calendar.py b/tests/components/local_calendar/test_calendar.py index 685623d193e31..559a2af38b325 100644 --- a/tests/components/local_calendar/test_calendar.py +++ b/tests/components/local_calendar/test_calendar.py @@ -1,7 +1,6 @@ """Tests for calendar platform of local calendar.""" import datetime -import logging import textwrap import pytest @@ -21,8 +20,6 @@ from tests.common import MockConfigEntry -_LOGGER = logging.getLogger(__name__) - async def test_empty_calendar( hass: HomeAssistant, setup_integration: None, get_events: GetEventsFn diff --git a/tests/components/local_calendar/test_diagnostics.py b/tests/components/local_calendar/test_diagnostics.py index 6a2696cafe840..9a1da25d77014 100644 --- a/tests/components/local_calendar/test_diagnostics.py +++ b/tests/components/local_calendar/test_diagnostics.py @@ -8,13 +8,25 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .conftest import TEST_ENTITY, Client, generate_new_hass_access_token +from .conftest import TEST_ENTITY, Client -from tests.common import MockConfigEntry, MockUser +from tests.common import CLIENT_ID, MockConfigEntry, MockUser from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator, WebSocketGenerator +async def generate_new_hass_access_token( + hass: HomeAssistant, hass_admin_user: MockUser, hass_admin_credential: Credentials +) -> str: + """Return an access token to access Home Assistant.""" + await hass.auth.async_link_user(hass_admin_user, hass_admin_credential) + + refresh_token = await hass.auth.async_create_refresh_token( + hass_admin_user, CLIENT_ID, credential=hass_admin_credential + ) + return hass.auth.async_create_access_token(refresh_token) + + def _get_test_client_generator( hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, new_token: str ): From 086762782537e0575de60b4cfe49bd6e401e05ea Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 8 Aug 2023 14:32:30 +0000 Subject: [PATCH 3/6] Revert unnecessary changes --- homeassistant/components/calendar/__init__.py | 2 +- homeassistant/components/nest/event.py | 106 ------------------ 2 files changed, 1 insertion(+), 107 deletions(-) delete mode 100644 homeassistant/components/nest/event.py diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index b81c34f272b00..be551e336312b 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -508,7 +508,6 @@ def state_attributes(self) -> dict[str, Any] | None: @property def state(self) -> str: """Return the state of the calendar event.""" - _LOGGER.debug("state property called: %s", self.event) if (event := self.event) is None: return STATE_OFF @@ -694,6 +693,7 @@ async def handle_calendar_event_create( ) ) return + try: await entity.async_create_event(**msg[CONF_EVENT]) except HomeAssistantError as ex: diff --git a/homeassistant/components/nest/event.py b/homeassistant/components/nest/event.py deleted file mode 100644 index 78025dc22b4ed..0000000000000 --- a/homeassistant/components/nest/event.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Support for Google Nest Events for Cameras.""" - -import logging - -from google_nest_sdm.camera_traits import ( - CameraMotionTrait, - CameraPersonTrait, - CameraSoundTrait, -) -from google_nest_sdm.device import Device -from google_nest_sdm.device_manager import DeviceManager -from google_nest_sdm.doorbell_traits import DoorbellChimeTrait -from google_nest_sdm.event import EventMessage - -from homeassistant.components.event import ( - EventDeviceClass, - EventEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import DATA_DEVICE_MANAGER, DOMAIN -from .device_info import NestDeviceInfo - -_LOGGER = logging.getLogger(__name__) - -DOORBELL_EVENT_TRAINTS = (DoorbellChimeTrait.NAME,) -MOTION_EVENT_TRAINTS = (CameraMotionTrait, CameraPersonTrait, CameraSoundTrait) - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up the cameras.""" - - device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ - DATA_DEVICE_MANAGER - ] - entities = [] - for device in device_manager.devices.values(): - if any(trait in device.traits for trait in DOORBELL_EVENT_TRAINTS): - entities.append(DoorbellEvent(device)) - - async_add_entities(entities) - - -class DoorbellEvent(EventEntity): - """Entity for handling doorbell press events.""" - - def __init__(self, device: Device) -> None: - """Initialize DoorbellEvent.""" - self._device = device - self._device_info = NestDeviceInfo(device) - self._attr_has_entity_name = True - self._attr_translation_key = "doorbell" - self._attr_unique_id = f"{device.name}-{self.device_class}" - self._attr_device_info = self._device_info.device_info - self._attr_device_class = EventDeviceClass.DOORBELL - self._attr_event_types = ["press"] - - async def _async_handle_event(self, event_message: EventMessage) -> None: - """Handle a doorbell press event.""" - if not event_message.resource_update_events: - return - if DoorbellChimeTrait.EVENT_NAME in event_message.resource_update_events: - self._trigger_event("press") - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Register callbacks with your device API/library.""" - self._device.add_event_callback(self._async_handle_event) - - -# 2023-08-06 23:32:04.565 DEBUG (MainThread) [google_nest_sdm.event] EventMessage raw_data={ -# 'eventId': '86539e37-1beb-4d75-83f8-a38612c20fd7', 'timestamp': '2023-08-06T23:32:01.727Z', -# 'resourceUpdate': { -# 'name': 'enterprises/9e664211-78d7-4e70-936c-a01f14179e36/devices/AVPHwEtV3Lm25VPuln9abzjLCJx6fBD-_dK1FWFYVTbvlQKWi8lBuCkapDdT1HmBZb89OR0HK7oaBfSgoPg7rkXMn3x9nA', -# 'events': { -# 'sdm.devices.events.DoorbellChime.Chime': { -# 'eventSessionId': '1632710533', 'eventId': 'n:2' -# }, -# 'sdm.devices.events.CameraClipPreview.ClipPreview': { -# 'eventSessionId': '1632710533', -# 'previewUrl': 'https://nest-camera-frontend.googleapis.com/frontend/encrypted/clippreview/AXG5WmjWL4kdZadtV-PksmouP-2PUTk8jwg0TOJrnB6YR4cXqC2I1Feq5bOdAQsOJ_BjCi0MD0Wv1OAQS1jdAuQhZ7Rio9ufEGpaqju_4ZDO5KzhCO5u-HJbXh7LXNb5dXVKeXGPuUAXDwlY5wc7ufddlN4cEUin5PntscIbVkUXl2YVKt84OnTfTu6ScF4dvzS6zptF9ghGd3rg0gHWGarZtia-WZr-PYFZhlZCJOzb3fe7V0wuZjPTx2TCNI_mA9ItF5GozTH4BnUW6nCpYVMf7yvFgqeScqIRCKcQ0yiCa5gkMcdImQ=='}}}, -# 'userId': 'AVPHwEvuuYrColDDTr3e1FU4cTrF8bdIA8O9JmZ4aUq8', -# 'eventThreadId': '21f8e530-c176-4eb5-ab3f-e574d4c224ec', -# 'resourceGroup': ['enterprises/9e664211-78d7-4e70-936c-a01f14179e36/devices/AVPHwEtV3Lm25VPuln9abzjLCJx6fBD-_dK1FWFYVTbvlQKWi8lBuCkapDdT1HmBZb89OR0HK7oaBfSgoPg7rkXMn3x9nA'], -# 'eventThreadState': 'STARTED' -# } - -# 2023-08-06 23:32:24.578 DEBUG (MainThread) [google_nest_sdm.event] EventMessage raw_data={ -# 'eventId': '86539e37-1beb-4d75-83f8-a38612c20fd7', 'timestamp': '2023-08-06T23:32:01.727Z', -# 'resourceUpdate': { -# 'name': 'enterprises/9e664211-78d7-4e70-936c-a01f14179e36/devices/AVPHwEtV3Lm25VPuln9abzjLCJx6fBD-_dK1FWFYVTbvlQKWi8lBuCkapDdT1HmBZb89OR0HK7oaBfSgoPg7rkXMn3x9nA', -# 'events': { -# 'sdm.devices.events.DoorbellChime.Chime': { -# 'eventSessionId': '1632710533', 'eventId': 'n:2' -# }, -# 'sdm.devices.events.CameraClipPreview.ClipPreview': { -# 'eventSessionId': '1632710533', 'previewUrl': 'https://nest-camera-frontend.googleapis.com/frontend/encrypted/clippreview/AXG5WmjWL4kdZadtV-PksmouP-2PUTk8jwg0TOJrnB6YR4cXqC2I1Feq5bOdAQsOJ_BjCi0MD0Wv1OAQS1jdAuQhZ7Rio9ufEGpaqju_4ZDO5KzhCO5u-HJbXh7LXNb5dXVKeXGPuUAXDwlY5wc7ufddlN4cEUin5PntscIbVkUXl2YVKt84OnTfTu6ScF4dvzS6zptF9ghGd3rg0gHWGarZtia-WZr-PYFZhlZCJOzb3fe7V0wuZjPTx2TCNI_mA9ItF5GozTH4BnUW6nCpYVMf7yvFgqeScqIRCKcQ0yiCa5gkMcdImQ=='}}}, -# 'userId': 'AVPHwEvuuYrColDDTr3e1FU4cTrF8bdIA8O9JmZ4aUq8', -# 'eventThreadId': '21f8e530-c176-4eb5-ab3f-e574d4c224ec', -# 'resourceGroup': ['enterprises/9e664211-78d7-4e70-936c-a01f14179e36/devices/AVPHwEtV3Lm25VPuln9abzjLCJx6fBD-_dK1FWFYVTbvlQKWi8lBuCkapDdT1HmBZb89OR0HK7oaBfSgoPg7rkXMn3x9nA'], -# 'eventThreadState': 'ENDED' -# } From 19f760f2ffaaed1f4c1cffd892f78650d81235db Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 12 Aug 2023 19:38:12 +0000 Subject: [PATCH 4/6] Move demo calendar to config entries to cleanup event timers --- homeassistant/components/demo/__init__.py | 2 +- homeassistant/components/demo/calendar.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 04eba5f05863a..b40e1ede23246 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -26,6 +26,7 @@ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CAMERA, + Platform.CALENDAR, Platform.CLIMATE, Platform.COVER, Platform.DATE, @@ -54,7 +55,6 @@ Platform.MAILBOX, Platform.NOTIFY, Platform.IMAGE_PROCESSING, - Platform.CALENDAR, Platform.DEVICE_TRACKER, Platform.WEATHER, ] diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 73b45a556404f..f35e3ec95f718 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -4,20 +4,19 @@ import datetime from homeassistant.components.calendar import CalendarEntity, CalendarEvent +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the Demo Calendar platform.""" - add_entities( + """Set up the Demo Calendar config entry.""" + async_add_entities( [ DemoCalendar(calendar_data_future(), "Calendar 1"), DemoCalendar(calendar_data_current(), "Calendar 2"), From 4f28e41bf7ec7c467bf356a7189f6bfd20afc396 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 12 Aug 2023 19:39:00 +0000 Subject: [PATCH 5/6] Fix docs on calendars --- homeassistant/components/demo/calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index f35e3ec95f718..b4200f1be8915 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -1,4 +1,4 @@ -"""Demo platform that has two fake binary sensors.""" +"""Demo platform that has two fake calendars.""" from __future__ import annotations import datetime From 249f115e367eb7a1a7e5935c514ba3b876285d59 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 19 Aug 2023 05:14:09 +0000 Subject: [PATCH 6/6] Move method inside from PR feedback --- homeassistant/components/calendar/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index be551e336312b..e487569453fa7 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -534,24 +534,24 @@ def async_write_ha_state(self) -> None: event = self.event if event is None or now >= event.end_datetime_local: return + + @callback + def update(_: datetime.datetime) -> None: + """Run when the active or upcoming event starts or ends.""" + self._async_write_ha_state() + if now < event.start_datetime_local: self._alarm_unsubs.append( async_track_point_in_time( self.hass, - self._event_start_or_end_trigger, + update, event.start_datetime_local, ) ) self._alarm_unsubs.append( - async_track_point_in_time( - self.hass, self._event_start_or_end_trigger, event.end_datetime_local - ) + async_track_point_in_time(self.hass, update, event.end_datetime_local) ) - async def _event_start_or_end_trigger(self, _: datetime.datetime) -> None: - """Run when the active or upcoming event starts or ends.""" - self._async_write_ha_state() - async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.