From ff135ecdc667d6bd833719368c38637d3fc848e8 Mon Sep 17 00:00:00 2001 From: Aaron Bach <bachya1208@gmail.com> Date: Tue, 28 Mar 2023 01:31:36 -0600 Subject: [PATCH] Add a calendar entity to Ridwell (#88108) * Subclass a `DataUpdateCoordinator` for Ridwell * Add a calendar entity to Ridwell * Simpler unique ID * Fix tests * Docstring --- .coveragerc | 1 + homeassistant/components/ridwell/__init__.py | 2 +- homeassistant/components/ridwell/calendar.py | 78 +++++++++++++++++++ .../components/ridwell/coordinator.py | 8 +- .../components/ridwell/diagnostics.py | 6 +- homeassistant/components/ridwell/entity.py | 15 ++-- homeassistant/components/ridwell/sensor.py | 7 +- homeassistant/components/ridwell/switch.py | 16 +++- tests/components/ridwell/conftest.py | 21 +++-- tests/components/ridwell/test_diagnostics.py | 2 +- 10 files changed, 131 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/ridwell/calendar.py diff --git a/.coveragerc b/.coveragerc index 520b87b08b9..dfc13304b19 100644 --- a/.coveragerc +++ b/.coveragerc @@ -997,6 +997,7 @@ omit = homeassistant/components/rest/notify.py homeassistant/components/rest/switch.py homeassistant/components/ridwell/__init__.py + homeassistant/components/ridwell/calendar.py homeassistant/components/ridwell/coordinator.py homeassistant/components/ridwell/switch.py homeassistant/components/ring/camera.py diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 116528f4ca8..1b0a83f1c05 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -11,7 +11,7 @@ from homeassistant.helpers import entity_registry as er from .const import DOMAIN, LOGGER, SENSOR_TYPE_NEXT_PICKUP from .coordinator import RidwellDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH] +PLATFORMS: list[Platform] = [Platform.CALENDAR, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/ridwell/calendar.py b/homeassistant/components/ridwell/calendar.py new file mode 100644 index 00000000000..57919ed1feb --- /dev/null +++ b/homeassistant/components/ridwell/calendar.py @@ -0,0 +1,78 @@ +"""Support for Ridwell calendars.""" +from __future__ import annotations + +import datetime + +from aioridwell.model import RidwellAccount, RidwellPickupEvent + +from homeassistant.components.calendar import CalendarEntity, CalendarEvent +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import RidwellDataUpdateCoordinator +from .entity import RidwellEntity + + +@callback +def async_get_calendar_event_from_pickup_event( + pickup_event: RidwellPickupEvent, +) -> CalendarEvent: + """Get a HASS CalendarEvent from an aioridwell PickupEvent.""" + pickup_type_string = ", ".join( + [ + f"{pickup.name} (quantity: {pickup.quantity})" + for pickup in pickup_event.pickups + ] + ) + return CalendarEvent( + summary=f"Ridwell Pickup ({pickup_event.state.value})", + description=f"Pickup types: {pickup_type_string}", + start=pickup_event.pickup_date, + end=pickup_event.pickup_date + datetime.timedelta(days=1), + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Ridwell calendars based on a config entry.""" + coordinator: RidwellDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + RidwellCalendar(coordinator, account) + for account in coordinator.accounts.values() + ) + + +class RidwellCalendar(RidwellEntity, CalendarEntity): + """Define a Ridwell calendar.""" + + _attr_icon = "mdi:delete-empty" + + def __init__( + self, coordinator: RidwellDataUpdateCoordinator, account: RidwellAccount + ) -> None: + """Initialize the Ridwell entity.""" + super().__init__(coordinator, account) + + self._attr_unique_id = self._account.account_id + self._event: CalendarEvent | None = None + + @property + def event(self) -> CalendarEvent | None: + """Return the next upcoming event.""" + return async_get_calendar_event_from_pickup_event(self.next_pickup_event) + + async def async_get_events( + self, + hass: HomeAssistant, + start_date: datetime.datetime, + end_date: datetime.datetime, + ) -> list[CalendarEvent]: + """Return calendar events within a datetime range.""" + return [ + async_get_calendar_event_from_pickup_event(event) + for event in self.coordinator.data[self._account.account_id] + ] diff --git a/homeassistant/components/ridwell/coordinator.py b/homeassistant/components/ridwell/coordinator.py index a3b83c70aae..9561cd26e4b 100644 --- a/homeassistant/components/ridwell/coordinator.py +++ b/homeassistant/components/ridwell/coordinator.py @@ -22,14 +22,14 @@ UPDATE_INTERVAL = timedelta(hours=1) class RidwellDataUpdateCoordinator( - DataUpdateCoordinator[dict[str, RidwellPickupEvent]] + DataUpdateCoordinator[dict[str, list[RidwellPickupEvent]]] ): """Class to manage fetching data from single endpoint.""" config_entry: ConfigEntry def __init__(self, hass: HomeAssistant, *, name: str) -> None: - """Initialize global data updater.""" + """Initialize.""" # These will be filled in by async_initialize; we give them these defaults to # avoid arduous typing checks down the line: self.accounts: dict[str, RidwellAccount] = {} @@ -38,13 +38,13 @@ class RidwellDataUpdateCoordinator( super().__init__(hass, LOGGER, name=name, update_interval=UPDATE_INTERVAL) - async def _async_update_data(self) -> dict[str, RidwellPickupEvent]: + async def _async_update_data(self) -> dict[str, list[RidwellPickupEvent]]: """Fetch the latest data from the source.""" data = {} async def async_get_pickups(account: RidwellAccount) -> None: """Get the latest pickups for an account.""" - data[account.account_id] = await account.async_get_next_pickup_event() + data[account.account_id] = await account.async_get_pickup_events() tasks = [async_get_pickups(account) for account in self.accounts.values()] results = await asyncio.gather(*tasks, return_exceptions=True) diff --git a/homeassistant/components/ridwell/diagnostics.py b/homeassistant/components/ridwell/diagnostics.py index 772efb87ac7..f48861cee19 100644 --- a/homeassistant/components/ridwell/diagnostics.py +++ b/homeassistant/components/ridwell/diagnostics.py @@ -32,7 +32,11 @@ async def async_get_config_entry_diagnostics( return async_redact_data( { "entry": entry.as_dict(), - "data": [dataclasses.asdict(event) for event in coordinator.data.values()], + "data": [ + dataclasses.asdict(event) + for events in coordinator.data.values() + for event in events + ], }, TO_REDACT, ) diff --git a/homeassistant/components/ridwell/entity.py b/homeassistant/components/ridwell/entity.py index 29dd68e2a81..9c7ceee7f56 100644 --- a/homeassistant/components/ridwell/entity.py +++ b/homeassistant/components/ridwell/entity.py @@ -1,8 +1,12 @@ """Define a base Ridwell entity.""" +from __future__ import annotations + +from datetime import date + from aioridwell.model import RidwellAccount, RidwellPickupEvent from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN @@ -18,7 +22,6 @@ class RidwellEntity(CoordinatorEntity[RidwellDataUpdateCoordinator]): self, coordinator: RidwellDataUpdateCoordinator, account: RidwellAccount, - description: EntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) @@ -31,10 +34,12 @@ class RidwellEntity(CoordinatorEntity[RidwellDataUpdateCoordinator]): manufacturer="Ridwell", name="Ridwell", ) - self._attr_unique_id = f"{account.account_id}_{description.key}" - self.entity_description = description @property def next_pickup_event(self) -> RidwellPickupEvent: """Get the next pickup event.""" - return self.coordinator.data[self._account.account_id] + return next( + event + for event in self.coordinator.data[self._account.account_id] + if event.pickup_date >= date.today() + ) diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index 05cee54ba9d..1eba555e955 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -27,7 +27,7 @@ ATTR_QUANTITY = "quantity" SENSOR_DESCRIPTION = SensorEntityDescription( key=SENSOR_TYPE_NEXT_PICKUP, - name="Ridwell pickup", + name="Next Ridwell pickup", device_class=SensorDeviceClass.DATE, ) @@ -54,9 +54,10 @@ class RidwellSensor(RidwellEntity, SensorEntity): description: SensorEntityDescription, ) -> None: """Initialize.""" - super().__init__(coordinator, account, description) + super().__init__(coordinator, account) - self._attr_name = f"{description.name} ({account.address['street1']})" + self._attr_unique_id = f"{account.account_id}_{description.key}" + self.entity_description = description @property def extra_state_attributes(self) -> Mapping[str, Any]: diff --git a/homeassistant/components/ridwell/switch.py b/homeassistant/components/ridwell/switch.py index f16bbaebab6..7a948f8b883 100644 --- a/homeassistant/components/ridwell/switch.py +++ b/homeassistant/components/ridwell/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from aioridwell.errors import RidwellError -from aioridwell.model import EventState +from aioridwell.model import EventState, RidwellAccount from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry @@ -38,7 +38,19 @@ async def async_setup_entry( class RidwellSwitch(RidwellEntity, SwitchEntity): - """Define a Ridwell button.""" + """Define a Ridwell switch.""" + + def __init__( + self, + coordinator: RidwellDataUpdateCoordinator, + account: RidwellAccount, + description: SwitchEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator, account) + + self._attr_unique_id = f"{account.account_id}_{description.key}" + self.entity_description = description @property def is_on(self) -> bool: diff --git a/tests/components/ridwell/conftest.py b/tests/components/ridwell/conftest.py index e86243da533..57d485d4281 100644 --- a/tests/components/ridwell/conftest.py +++ b/tests/components/ridwell/conftest.py @@ -3,6 +3,7 @@ from datetime import date from unittest.mock import AsyncMock, Mock, patch from aioridwell.model import EventState, RidwellPickup, RidwellPickupEvent +from freezegun import freeze_time import pytest from homeassistant.components.ridwell.const import DOMAIN @@ -28,14 +29,16 @@ def account_fixture(): "state": "New York", "postal_code": "10001", }, - async_get_next_pickup_event=AsyncMock( - return_value=RidwellPickupEvent( - None, - "event_123", - date(2022, 1, 24), - [RidwellPickup("Plastic Film", "offer_123", 1, "product_123", 1)], - EventState.INITIALIZED, - ) + async_get_pickup_events=AsyncMock( + return_value=[ + RidwellPickupEvent( + None, + "event_123", + date(2022, 1, 24), + [RidwellPickup("Plastic Film", "offer_123", 1, "product_123", 1)], + EventState.INITIALIZED, + ) + ] ), ) @@ -77,6 +80,8 @@ async def mock_aioridwell_fixture(hass, client, config): ), patch( "homeassistant.components.ridwell.coordinator.async_get_client", return_value=client, + ), freeze_time( + "2022-01-01" ): yield diff --git a/tests/components/ridwell/test_diagnostics.py b/tests/components/ridwell/test_diagnostics.py index e73b352f3d9..caac4880417 100644 --- a/tests/components/ridwell/test_diagnostics.py +++ b/tests/components/ridwell/test_diagnostics.py @@ -32,7 +32,7 @@ async def test_entry_diagnostics( "_async_request": None, "event_id": "event_123", "pickup_date": { - "__type": "<class 'datetime.date'>", + "__type": "<class 'freezegun.api.FakeDate'>", "isoformat": "2022-01-24", }, "pickups": [ -- GitLab