Add DataUpdateCoordinator to the Todoist integration (#89836)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
Aaron Godfrey 2023-03-28 09:57:24 -07:00 committed by GitHub
parent 89a3c304c2
commit 9ccd43e5f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 72 additions and 11 deletions

View File

@ -23,6 +23,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt
from .const import (
@ -54,6 +55,7 @@ from .const import (
START,
SUMMARY,
)
from .coordinator import TodoistCoordinator
from .types import CalData, CustomProject, ProjectData, TodoistEvent
_LOGGER = logging.getLogger(__name__)
@ -117,6 +119,8 @@ async def async_setup_platform(
project_id_lookup = {}
api = TodoistAPIAsync(token)
coordinator = TodoistCoordinator(hass, _LOGGER, SCAN_INTERVAL, api)
await coordinator.async_config_entry_first_refresh()
# Setup devices:
# Grab all projects.
@ -131,7 +135,7 @@ async def async_setup_platform(
# Project is an object, not a dict!
# Because of that, we convert what we need to a dict.
project_data: ProjectData = {CONF_NAME: project.name, CONF_ID: project.id}
project_devices.append(TodoistProjectEntity(project_data, labels, api))
project_devices.append(TodoistProjectEntity(coordinator, project_data, labels))
# Cache the names so we can easily look up name->ID.
project_id_lookup[project.name.lower()] = project.id
@ -157,9 +161,9 @@ async def async_setup_platform(
# Create the custom project and add it to the devices array.
project_devices.append(
TodoistProjectEntity(
coordinator,
{"id": None, "name": extra_project["name"]},
labels,
api,
due_date_days=project_due_date,
whitelisted_labels=project_label_filter,
whitelisted_projects=project_id_filter,
@ -267,23 +271,24 @@ async def async_setup_platform(
)
class TodoistProjectEntity(CalendarEntity):
class TodoistProjectEntity(CoordinatorEntity[TodoistCoordinator], CalendarEntity):
"""A device for getting the next Task from a Todoist Project."""
def __init__(
self,
coordinator: TodoistCoordinator,
data: ProjectData,
labels: list[Label],
api: TodoistAPIAsync,
due_date_days: int | None = None,
whitelisted_labels: list[str] | None = None,
whitelisted_projects: list[str] | None = None,
) -> None:
"""Create the Todoist Calendar Entity."""
super().__init__(coordinator=coordinator)
self.data = TodoistProjectData(
data,
labels,
api,
coordinator,
due_date_days=due_date_days,
whitelisted_labels=whitelisted_labels,
whitelisted_projects=whitelisted_projects,
@ -306,6 +311,7 @@ class TodoistProjectEntity(CalendarEntity):
async def async_update(self) -> None:
"""Update all Todoist Calendars."""
await super().async_update()
await self.data.async_update()
# Set Todoist-specific data that can't easily be grabbed
self._cal_data["all_tasks"] = [
@ -373,7 +379,7 @@ class TodoistProjectData:
self,
project_data: ProjectData,
labels: list[Label],
api: TodoistAPIAsync,
coordinator: TodoistCoordinator,
due_date_days: int | None = None,
whitelisted_labels: list[str] | None = None,
whitelisted_projects: list[str] | None = None,
@ -381,7 +387,7 @@ class TodoistProjectData:
"""Initialize a Todoist Project."""
self.event: TodoistEvent | None = None
self._api = api
self._coordinator = coordinator
self._name = project_data[CONF_NAME]
# If no ID is defined, fetch all tasks.
self._id = project_data.get(CONF_ID)
@ -569,8 +575,8 @@ class TodoistProjectData:
self, start_date: datetime, end_date: datetime
) -> list[CalendarEvent]:
"""Get all tasks in a specific time frame."""
tasks = self._coordinator.data
if self._id is None:
tasks = await self._api.get_tasks()
project_task_data = [
task
for task in tasks
@ -578,7 +584,7 @@ class TodoistProjectData:
or task.project_id in self._project_id_whitelist
]
else:
project_task_data = await self._api.get_tasks(project_id=self._id)
project_task_data = [task for task in tasks if task.project_id == self._id]
events = []
for task in project_task_data:
@ -607,8 +613,8 @@ class TodoistProjectData:
async def async_update(self) -> None:
"""Get the latest data."""
tasks = self._coordinator.data
if self._id is None:
tasks = await self._api.get_tasks()
project_task_data = [
task
for task in tasks
@ -616,7 +622,7 @@ class TodoistProjectData:
or task.project_id in self._project_id_whitelist
]
else:
project_task_data = await self._api.get_tasks(project_id=self._id)
project_task_data = [task for task in tasks if task.project_id == self._id]
# If we have no data, we can just return right away.
if not project_task_data:

View File

@ -0,0 +1,31 @@
"""DataUpdateCoordinator for the Todoist component."""
from datetime import timedelta
import logging
from todoist_api_python.api_async import TodoistAPIAsync
from todoist_api_python.models import Task
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
class TodoistCoordinator(DataUpdateCoordinator[list[Task]]):
"""Coordinator for updating task data from Todoist."""
def __init__(
self,
hass: HomeAssistant,
logger: logging.Logger,
update_interval: timedelta,
api: TodoistAPIAsync,
) -> None:
"""Initialize the Todoist coordinator."""
super().__init__(hass, logger, name="Todoist", update_interval=update_interval)
self.api = api
async def _async_update_data(self) -> list[Task]:
"""Fetch tasks from the Todoist API."""
try:
return await self.api.get_tasks()
except Exception as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err

View File

@ -132,6 +132,30 @@ async def test_update_entity_for_custom_project_with_labels_on(
assert state.state == "on"
@patch("homeassistant.components.todoist.calendar.TodoistAPIAsync")
async def test_failed_coordinator_update(todoist_api, hass: HomeAssistant, api) -> None:
"""Test a failed data coordinator update is handled correctly."""
api.get_tasks.side_effect = Exception("API error")
todoist_api.return_value = api
assert await setup.async_setup_component(
hass,
"calendar",
{
"calendar": {
"platform": DOMAIN,
CONF_TOKEN: "token",
"custom_projects": [{"name": "All projects", "labels": ["Label1"]}],
}
},
)
await hass.async_block_till_done()
await async_update_entity(hass, "calendar.all_projects")
state = hass.states.get("calendar.all_projects")
assert state is None
@patch("homeassistant.components.todoist.calendar.TodoistAPIAsync")
async def test_calendar_custom_project_unique_id(
todoist_api, hass: HomeAssistant, api, entity_registry: er.EntityRegistry