Add DataUpdateCoordinator to the Todoist integration (#89836)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
89a3c304c2
commit
9ccd43e5f1
3 changed files with 72 additions and 11 deletions
|
@ -23,6 +23,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.util import dt
|
from homeassistant.util import dt
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
@ -54,6 +55,7 @@ from .const import (
|
||||||
START,
|
START,
|
||||||
SUMMARY,
|
SUMMARY,
|
||||||
)
|
)
|
||||||
|
from .coordinator import TodoistCoordinator
|
||||||
from .types import CalData, CustomProject, ProjectData, TodoistEvent
|
from .types import CalData, CustomProject, ProjectData, TodoistEvent
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -117,6 +119,8 @@ async def async_setup_platform(
|
||||||
project_id_lookup = {}
|
project_id_lookup = {}
|
||||||
|
|
||||||
api = TodoistAPIAsync(token)
|
api = TodoistAPIAsync(token)
|
||||||
|
coordinator = TodoistCoordinator(hass, _LOGGER, SCAN_INTERVAL, api)
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
# Setup devices:
|
# Setup devices:
|
||||||
# Grab all projects.
|
# Grab all projects.
|
||||||
|
@ -131,7 +135,7 @@ async def async_setup_platform(
|
||||||
# Project is an object, not a dict!
|
# Project is an object, not a dict!
|
||||||
# Because of that, we convert what we need to 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_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.
|
# Cache the names so we can easily look up name->ID.
|
||||||
project_id_lookup[project.name.lower()] = project.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.
|
# Create the custom project and add it to the devices array.
|
||||||
project_devices.append(
|
project_devices.append(
|
||||||
TodoistProjectEntity(
|
TodoistProjectEntity(
|
||||||
|
coordinator,
|
||||||
{"id": None, "name": extra_project["name"]},
|
{"id": None, "name": extra_project["name"]},
|
||||||
labels,
|
labels,
|
||||||
api,
|
|
||||||
due_date_days=project_due_date,
|
due_date_days=project_due_date,
|
||||||
whitelisted_labels=project_label_filter,
|
whitelisted_labels=project_label_filter,
|
||||||
whitelisted_projects=project_id_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."""
|
"""A device for getting the next Task from a Todoist Project."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
coordinator: TodoistCoordinator,
|
||||||
data: ProjectData,
|
data: ProjectData,
|
||||||
labels: list[Label],
|
labels: list[Label],
|
||||||
api: TodoistAPIAsync,
|
|
||||||
due_date_days: int | None = None,
|
due_date_days: int | None = None,
|
||||||
whitelisted_labels: list[str] | None = None,
|
whitelisted_labels: list[str] | None = None,
|
||||||
whitelisted_projects: list[str] | None = None,
|
whitelisted_projects: list[str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create the Todoist Calendar Entity."""
|
"""Create the Todoist Calendar Entity."""
|
||||||
|
super().__init__(coordinator=coordinator)
|
||||||
self.data = TodoistProjectData(
|
self.data = TodoistProjectData(
|
||||||
data,
|
data,
|
||||||
labels,
|
labels,
|
||||||
api,
|
coordinator,
|
||||||
due_date_days=due_date_days,
|
due_date_days=due_date_days,
|
||||||
whitelisted_labels=whitelisted_labels,
|
whitelisted_labels=whitelisted_labels,
|
||||||
whitelisted_projects=whitelisted_projects,
|
whitelisted_projects=whitelisted_projects,
|
||||||
|
@ -306,6 +311,7 @@ class TodoistProjectEntity(CalendarEntity):
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update all Todoist Calendars."""
|
"""Update all Todoist Calendars."""
|
||||||
|
await super().async_update()
|
||||||
await self.data.async_update()
|
await self.data.async_update()
|
||||||
# Set Todoist-specific data that can't easily be grabbed
|
# Set Todoist-specific data that can't easily be grabbed
|
||||||
self._cal_data["all_tasks"] = [
|
self._cal_data["all_tasks"] = [
|
||||||
|
@ -373,7 +379,7 @@ class TodoistProjectData:
|
||||||
self,
|
self,
|
||||||
project_data: ProjectData,
|
project_data: ProjectData,
|
||||||
labels: list[Label],
|
labels: list[Label],
|
||||||
api: TodoistAPIAsync,
|
coordinator: TodoistCoordinator,
|
||||||
due_date_days: int | None = None,
|
due_date_days: int | None = None,
|
||||||
whitelisted_labels: list[str] | None = None,
|
whitelisted_labels: list[str] | None = None,
|
||||||
whitelisted_projects: list[str] | None = None,
|
whitelisted_projects: list[str] | None = None,
|
||||||
|
@ -381,7 +387,7 @@ class TodoistProjectData:
|
||||||
"""Initialize a Todoist Project."""
|
"""Initialize a Todoist Project."""
|
||||||
self.event: TodoistEvent | None = None
|
self.event: TodoistEvent | None = None
|
||||||
|
|
||||||
self._api = api
|
self._coordinator = coordinator
|
||||||
self._name = project_data[CONF_NAME]
|
self._name = project_data[CONF_NAME]
|
||||||
# If no ID is defined, fetch all tasks.
|
# If no ID is defined, fetch all tasks.
|
||||||
self._id = project_data.get(CONF_ID)
|
self._id = project_data.get(CONF_ID)
|
||||||
|
@ -569,8 +575,8 @@ class TodoistProjectData:
|
||||||
self, start_date: datetime, end_date: datetime
|
self, start_date: datetime, end_date: datetime
|
||||||
) -> list[CalendarEvent]:
|
) -> list[CalendarEvent]:
|
||||||
"""Get all tasks in a specific time frame."""
|
"""Get all tasks in a specific time frame."""
|
||||||
|
tasks = self._coordinator.data
|
||||||
if self._id is None:
|
if self._id is None:
|
||||||
tasks = await self._api.get_tasks()
|
|
||||||
project_task_data = [
|
project_task_data = [
|
||||||
task
|
task
|
||||||
for task in tasks
|
for task in tasks
|
||||||
|
@ -578,7 +584,7 @@ class TodoistProjectData:
|
||||||
or task.project_id in self._project_id_whitelist
|
or task.project_id in self._project_id_whitelist
|
||||||
]
|
]
|
||||||
else:
|
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 = []
|
events = []
|
||||||
for task in project_task_data:
|
for task in project_task_data:
|
||||||
|
@ -607,8 +613,8 @@ class TodoistProjectData:
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Get the latest data."""
|
"""Get the latest data."""
|
||||||
|
tasks = self._coordinator.data
|
||||||
if self._id is None:
|
if self._id is None:
|
||||||
tasks = await self._api.get_tasks()
|
|
||||||
project_task_data = [
|
project_task_data = [
|
||||||
task
|
task
|
||||||
for task in tasks
|
for task in tasks
|
||||||
|
@ -616,7 +622,7 @@ class TodoistProjectData:
|
||||||
or task.project_id in self._project_id_whitelist
|
or task.project_id in self._project_id_whitelist
|
||||||
]
|
]
|
||||||
else:
|
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 we have no data, we can just return right away.
|
||||||
if not project_task_data:
|
if not project_task_data:
|
||||||
|
|
31
homeassistant/components/todoist/coordinator.py
Normal file
31
homeassistant/components/todoist/coordinator.py
Normal 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
|
|
@ -132,6 +132,30 @@ async def test_update_entity_for_custom_project_with_labels_on(
|
||||||
assert state.state == "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")
|
@patch("homeassistant.components.todoist.calendar.TodoistAPIAsync")
|
||||||
async def test_calendar_custom_project_unique_id(
|
async def test_calendar_custom_project_unique_id(
|
||||||
todoist_api, hass: HomeAssistant, api, entity_registry: er.EntityRegistry
|
todoist_api, hass: HomeAssistant, api, entity_registry: er.EntityRegistry
|
||||||
|
|
Loading…
Add table
Reference in a new issue