mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Improve Google Tasks coordinator updates behavior (#133316)
This commit is contained in:
parent
255f85eb2f
commit
a3ef3cce3e
@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
|
||||
from homeassistant.const import Platform
|
||||
@ -11,8 +13,9 @@ from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from . import api
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TaskUpdateCoordinator
|
||||
from .exceptions import GoogleTasksApiError
|
||||
from .types import GoogleTasksConfigEntry, GoogleTasksData
|
||||
from .types import GoogleTasksConfigEntry
|
||||
|
||||
__all__ = [
|
||||
"DOMAIN",
|
||||
@ -46,7 +49,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleTasksConfigEntry)
|
||||
except GoogleTasksApiError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
entry.runtime_data = GoogleTasksData(auth, task_lists)
|
||||
coordinators = [
|
||||
TaskUpdateCoordinator(
|
||||
hass,
|
||||
auth,
|
||||
task_list["id"],
|
||||
task_list["title"],
|
||||
)
|
||||
for task_list in task_lists
|
||||
]
|
||||
# Refresh all coordinators in parallel
|
||||
await asyncio.gather(
|
||||
*(
|
||||
coordinator.async_config_entry_first_refresh()
|
||||
for coordinator in coordinators
|
||||
)
|
||||
)
|
||||
entry.runtime_data = coordinators
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
@ -20,7 +20,11 @@ class TaskUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
|
||||
"""Coordinator for fetching Google Tasks for a Task List form the API."""
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, api: AsyncConfigEntryAuth, task_list_id: str
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
api: AsyncConfigEntryAuth,
|
||||
task_list_id: str,
|
||||
task_list_title: str,
|
||||
) -> None:
|
||||
"""Initialize TaskUpdateCoordinator."""
|
||||
super().__init__(
|
||||
@ -30,9 +34,10 @@ class TaskUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
)
|
||||
self.api = api
|
||||
self._task_list_id = task_list_id
|
||||
self.task_list_id = task_list_id
|
||||
self.task_list_title = task_list_title
|
||||
|
||||
async def _async_update_data(self) -> list[dict[str, Any]]:
|
||||
"""Fetch tasks from API endpoint."""
|
||||
async with asyncio.timeout(TIMEOUT):
|
||||
return await self.api.list_tasks(self._task_list_id)
|
||||
return await self.api.list_tasks(self.task_list_id)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, date, datetime, timedelta
|
||||
from datetime import UTC, date, datetime
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.todo import (
|
||||
@ -20,7 +20,6 @@ from .coordinator import TaskUpdateCoordinator
|
||||
from .types import GoogleTasksConfigEntry
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
SCAN_INTERVAL = timedelta(minutes=15)
|
||||
|
||||
TODO_STATUS_MAP = {
|
||||
"needsAction": TodoItemStatus.NEEDS_ACTION,
|
||||
@ -76,14 +75,13 @@ async def async_setup_entry(
|
||||
async_add_entities(
|
||||
(
|
||||
GoogleTaskTodoListEntity(
|
||||
TaskUpdateCoordinator(hass, entry.runtime_data.api, task_list["id"]),
|
||||
task_list["title"],
|
||||
coordinator,
|
||||
coordinator.task_list_title,
|
||||
entry.entry_id,
|
||||
task_list["id"],
|
||||
coordinator.task_list_id,
|
||||
)
|
||||
for task_list in entry.runtime_data.task_lists
|
||||
for coordinator in entry.runtime_data
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
@ -118,8 +116,6 @@ class GoogleTaskTodoListEntity(
|
||||
@property
|
||||
def todo_items(self) -> list[TodoItem] | None:
|
||||
"""Get the current set of To-do items."""
|
||||
if self.coordinator.data is None:
|
||||
return None
|
||||
return [_convert_api_item(item) for item in _order_tasks(self.coordinator.data)]
|
||||
|
||||
async def async_create_todo_item(self, item: TodoItem) -> None:
|
||||
|
@ -1,19 +1,7 @@
|
||||
"""Types for the Google Tasks integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
from .api import AsyncConfigEntryAuth
|
||||
from .coordinator import TaskUpdateCoordinator
|
||||
|
||||
|
||||
@dataclass
|
||||
class GoogleTasksData:
|
||||
"""Class to hold Google Tasks data."""
|
||||
|
||||
api: AsyncConfigEntryAuth
|
||||
task_lists: list[dict[str, Any]]
|
||||
|
||||
|
||||
type GoogleTasksConfigEntry = ConfigEntry[GoogleTasksData]
|
||||
type GoogleTasksConfigEntry = ConfigEntry[list[TaskUpdateCoordinator]]
|
||||
|
@ -34,6 +34,18 @@ LIST_TASK_LIST_RESPONSE = {
|
||||
"items": [TASK_LIST],
|
||||
}
|
||||
|
||||
LIST_TASKS_RESPONSE_WATER = {
|
||||
"items": [
|
||||
{
|
||||
"id": "some-task-id",
|
||||
"title": "Water",
|
||||
"status": "needsAction",
|
||||
"description": "Any size is ok",
|
||||
"position": "00000000000000000001",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
@ -44,7 +56,7 @@ def platforms() -> list[Platform]:
|
||||
@pytest.fixture(name="expires_at")
|
||||
def mock_expires_at() -> int:
|
||||
"""Fixture to set the oauth token expiration time."""
|
||||
return time.time() + 3600
|
||||
return time.time() + 86400
|
||||
|
||||
|
||||
@pytest.fixture(name="token_entry")
|
||||
|
@ -3,6 +3,7 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
import http
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import time
|
||||
from unittest.mock import Mock
|
||||
|
||||
@ -15,13 +16,15 @@ from homeassistant.components.google_tasks.const import OAUTH2_TOKEN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import LIST_TASK_LIST_RESPONSE
|
||||
from .conftest import LIST_TASK_LIST_RESPONSE, LIST_TASKS_RESPONSE_WATER
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
@pytest.mark.parametrize("api_responses", [[LIST_TASK_LIST_RESPONSE]])
|
||||
@pytest.mark.parametrize(
|
||||
"api_responses", [[LIST_TASK_LIST_RESPONSE, LIST_TASKS_RESPONSE_WATER]]
|
||||
)
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
@ -42,8 +45,10 @@ async def test_setup(
|
||||
assert not hass.services.async_services().get(DOMAIN)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expires_at", [time.time() - 3600], ids=["expired"])
|
||||
@pytest.mark.parametrize("api_responses", [[LIST_TASK_LIST_RESPONSE]])
|
||||
@pytest.mark.parametrize("expires_at", [time.time() - 86400], ids=["expired"])
|
||||
@pytest.mark.parametrize(
|
||||
"api_responses", [[LIST_TASK_LIST_RESPONSE, LIST_TASKS_RESPONSE_WATER]]
|
||||
)
|
||||
async def test_expired_token_refresh_success(
|
||||
hass: HomeAssistant,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
@ -60,8 +65,8 @@ async def test_expired_token_refresh_success(
|
||||
json={
|
||||
"access_token": "updated-access-token",
|
||||
"refresh_token": "updated-refresh-token",
|
||||
"expires_at": time.time() + 3600,
|
||||
"expires_in": 3600,
|
||||
"expires_at": time.time() + 86400,
|
||||
"expires_in": 86400,
|
||||
},
|
||||
)
|
||||
|
||||
@ -69,26 +74,26 @@ async def test_expired_token_refresh_success(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
assert config_entry.data["token"]["access_token"] == "updated-access-token"
|
||||
assert config_entry.data["token"]["expires_in"] == 3600
|
||||
assert config_entry.data["token"]["expires_in"] == 86400
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("expires_at", "status", "exc", "expected_state"),
|
||||
[
|
||||
(
|
||||
time.time() - 3600,
|
||||
time.time() - 86400,
|
||||
http.HTTPStatus.UNAUTHORIZED,
|
||||
None,
|
||||
ConfigEntryState.SETUP_ERROR,
|
||||
),
|
||||
(
|
||||
time.time() - 3600,
|
||||
time.time() - 86400,
|
||||
http.HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
None,
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
),
|
||||
(
|
||||
time.time() - 3600,
|
||||
time.time() - 86400,
|
||||
None,
|
||||
ClientError("error"),
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
@ -124,6 +129,16 @@ async def test_expired_token_refresh_failure(
|
||||
"response_handler",
|
||||
[
|
||||
([(Response({"status": HTTPStatus.INTERNAL_SERVER_ERROR}), b"")]),
|
||||
# First request succeeds, second request fails
|
||||
(
|
||||
[
|
||||
(
|
||||
Response({"status": HTTPStatus.OK}),
|
||||
json.dumps(LIST_TASK_LIST_RESPONSE),
|
||||
),
|
||||
(Response({"status": HTTPStatus.INTERNAL_SERVER_ERROR}), b""),
|
||||
]
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_setup_error(
|
||||
|
@ -6,10 +6,12 @@ import json
|
||||
from typing import Any
|
||||
from unittest.mock import Mock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from httplib2 import Response
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.google_tasks.coordinator import UPDATE_INTERVAL
|
||||
from homeassistant.components.todo import (
|
||||
ATTR_DESCRIPTION,
|
||||
ATTR_DUE_DATE,
|
||||
@ -19,12 +21,17 @@ from homeassistant.components.todo import (
|
||||
DOMAIN as TODO_DOMAIN,
|
||||
TodoServices,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .conftest import LIST_TASK_LIST_RESPONSE, create_response_object
|
||||
from .conftest import (
|
||||
LIST_TASK_LIST_RESPONSE,
|
||||
LIST_TASKS_RESPONSE_WATER,
|
||||
create_response_object,
|
||||
)
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
ENTITY_ID = "todo.my_tasks"
|
||||
@ -44,17 +51,6 @@ ERROR_RESPONSE = {
|
||||
CONTENT_ID = "Content-ID"
|
||||
BOUNDARY = "batch_00972cc8-75bd-11ee-9692-0242ac110002" # Arbitrary uuid
|
||||
|
||||
LIST_TASKS_RESPONSE_WATER = {
|
||||
"items": [
|
||||
{
|
||||
"id": "some-task-id",
|
||||
"title": "Water",
|
||||
"status": "needsAction",
|
||||
"description": "Any size is ok",
|
||||
"position": "00000000000000000001",
|
||||
},
|
||||
],
|
||||
}
|
||||
LIST_TASKS_RESPONSE_MULTIPLE = {
|
||||
"items": [
|
||||
{
|
||||
@ -311,7 +307,9 @@ async def test_empty_todo_list(
|
||||
[
|
||||
[
|
||||
LIST_TASK_LIST_RESPONSE,
|
||||
ERROR_RESPONSE,
|
||||
LIST_TASKS_RESPONSE_WATER,
|
||||
ERROR_RESPONSE, # Fail after one update interval
|
||||
LIST_TASKS_RESPONSE_WATER,
|
||||
]
|
||||
],
|
||||
)
|
||||
@ -319,18 +317,34 @@ async def test_task_items_error_response(
|
||||
hass: HomeAssistant,
|
||||
setup_credentials: None,
|
||||
integration_setup: Callable[[], Awaitable[bool]],
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
ws_get_items: Callable[[], Awaitable[dict[str, str]]],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test an error while getting todo list items."""
|
||||
"""Test an error while the entity updates getting a new list of todo list items."""
|
||||
|
||||
assert await integration_setup()
|
||||
|
||||
await hass_ws_client(hass)
|
||||
# Test successful setup and first data fetch
|
||||
state = hass.states.get("todo.my_tasks")
|
||||
assert state
|
||||
assert state.state == "1"
|
||||
|
||||
# Next update fails
|
||||
freezer.tick(UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("todo.my_tasks")
|
||||
assert state
|
||||
assert state.state == "unavailable"
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# Next update succeeds
|
||||
freezer.tick(UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("todo.my_tasks")
|
||||
assert state
|
||||
assert state.state == "1"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
Loading…
x
Reference in New Issue
Block a user