mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Add Google Tasks create and update for todo platform (#102754)
* Add Google Tasks create and update for todo platform * Update comments * Update comments
This commit is contained in:
parent
ffed1e8274
commit
7f7064ce59
@ -51,3 +51,31 @@ class AsyncConfigEntryAuth:
|
|||||||
)
|
)
|
||||||
result = await self._hass.async_add_executor_job(cmd.execute)
|
result = await self._hass.async_add_executor_job(cmd.execute)
|
||||||
return result["items"]
|
return result["items"]
|
||||||
|
|
||||||
|
async def insert(
|
||||||
|
self,
|
||||||
|
task_list_id: str,
|
||||||
|
task: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Create a new Task resource on the task list."""
|
||||||
|
service = await self._get_service()
|
||||||
|
cmd: HttpRequest = service.tasks().insert(
|
||||||
|
tasklist=task_list_id,
|
||||||
|
body=task,
|
||||||
|
)
|
||||||
|
await self._hass.async_add_executor_job(cmd.execute)
|
||||||
|
|
||||||
|
async def patch(
|
||||||
|
self,
|
||||||
|
task_list_id: str,
|
||||||
|
task_id: str,
|
||||||
|
task: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Update a task resource."""
|
||||||
|
service = await self._get_service()
|
||||||
|
cmd: HttpRequest = service.tasks().patch(
|
||||||
|
tasklist=task_list_id,
|
||||||
|
task=task_id,
|
||||||
|
body=task,
|
||||||
|
)
|
||||||
|
await self._hass.async_add_executor_job(cmd.execute)
|
||||||
|
@ -29,10 +29,10 @@ class TaskUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
name=f"Google Tasks {task_list_id}",
|
name=f"Google Tasks {task_list_id}",
|
||||||
update_interval=UPDATE_INTERVAL,
|
update_interval=UPDATE_INTERVAL,
|
||||||
)
|
)
|
||||||
self._api = api
|
self.api = api
|
||||||
self._task_list_id = task_list_id
|
self._task_list_id = task_list_id
|
||||||
|
|
||||||
async def _async_update_data(self) -> list[dict[str, Any]]:
|
async def _async_update_data(self) -> list[dict[str, Any]]:
|
||||||
"""Fetch tasks from API endpoint."""
|
"""Fetch tasks from API endpoint."""
|
||||||
async with asyncio.timeout(TIMEOUT):
|
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,8 +2,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
from homeassistant.components.todo import TodoItem, TodoItemStatus, TodoListEntity
|
from homeassistant.components.todo import (
|
||||||
|
TodoItem,
|
||||||
|
TodoItemStatus,
|
||||||
|
TodoListEntity,
|
||||||
|
TodoListEntityFeature,
|
||||||
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
@ -19,6 +25,17 @@ TODO_STATUS_MAP = {
|
|||||||
"needsAction": TodoItemStatus.NEEDS_ACTION,
|
"needsAction": TodoItemStatus.NEEDS_ACTION,
|
||||||
"completed": TodoItemStatus.COMPLETED,
|
"completed": TodoItemStatus.COMPLETED,
|
||||||
}
|
}
|
||||||
|
TODO_STATUS_MAP_INV = {v: k for k, v in TODO_STATUS_MAP.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_todo_item(item: TodoItem) -> dict[str, str]:
|
||||||
|
"""Convert TodoItem dataclass items to dictionary of attributes the tasks API."""
|
||||||
|
result: dict[str, str] = {}
|
||||||
|
if item.summary is not None:
|
||||||
|
result["title"] = item.summary
|
||||||
|
if item.status is not None:
|
||||||
|
result["status"] = TODO_STATUS_MAP_INV[item.status]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@ -45,6 +62,9 @@ class GoogleTaskTodoListEntity(CoordinatorEntity, TodoListEntity):
|
|||||||
"""A To-do List representation of the Shopping List."""
|
"""A To-do List representation of the Shopping List."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
_attr_supported_features = (
|
||||||
|
TodoListEntityFeature.CREATE_TODO_ITEM | TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -57,6 +77,7 @@ class GoogleTaskTodoListEntity(CoordinatorEntity, TodoListEntity):
|
|||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._attr_name = name.capitalize()
|
self._attr_name = name.capitalize()
|
||||||
self._attr_unique_id = f"{config_entry_id}-{task_list_id}"
|
self._attr_unique_id = f"{config_entry_id}-{task_list_id}"
|
||||||
|
self._task_list_id = task_list_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def todo_items(self) -> list[TodoItem] | None:
|
def todo_items(self) -> list[TodoItem] | None:
|
||||||
@ -73,3 +94,21 @@ class GoogleTaskTodoListEntity(CoordinatorEntity, TodoListEntity):
|
|||||||
)
|
)
|
||||||
for item in self.coordinator.data
|
for item in self.coordinator.data
|
||||||
]
|
]
|
||||||
|
|
||||||
|
async def async_create_todo_item(self, item: TodoItem) -> None:
|
||||||
|
"""Add an item to the To-do list."""
|
||||||
|
await self.coordinator.api.insert(
|
||||||
|
self._task_list_id,
|
||||||
|
task=_convert_todo_item(item),
|
||||||
|
)
|
||||||
|
await self.coordinator.async_refresh()
|
||||||
|
|
||||||
|
async def async_update_todo_item(self, item: TodoItem) -> None:
|
||||||
|
"""Update a To-do item."""
|
||||||
|
uid: str = cast(str, item.uid)
|
||||||
|
await self.coordinator.api.patch(
|
||||||
|
self._task_list_id,
|
||||||
|
uid,
|
||||||
|
task=_convert_todo_item(item),
|
||||||
|
)
|
||||||
|
await self.coordinator.async_refresh()
|
||||||
|
37
tests/components/google_tasks/snapshots/test_todo.ambr
Normal file
37
tests/components/google_tasks/snapshots/test_todo.ambr
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_create_todo_list_item[api_responses0]
|
||||||
|
tuple(
|
||||||
|
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks?alt=json',
|
||||||
|
'POST',
|
||||||
|
)
|
||||||
|
# ---
|
||||||
|
# name: test_create_todo_list_item[api_responses0].1
|
||||||
|
'{"title": "Soda", "status": "needsAction"}'
|
||||||
|
# ---
|
||||||
|
# name: test_partial_update_status[api_responses0]
|
||||||
|
tuple(
|
||||||
|
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
|
||||||
|
'PATCH',
|
||||||
|
)
|
||||||
|
# ---
|
||||||
|
# name: test_partial_update_status[api_responses0].1
|
||||||
|
'{"status": "needsAction"}'
|
||||||
|
# ---
|
||||||
|
# name: test_partial_update_title[api_responses0]
|
||||||
|
tuple(
|
||||||
|
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
|
||||||
|
'PATCH',
|
||||||
|
)
|
||||||
|
# ---
|
||||||
|
# name: test_partial_update_title[api_responses0].1
|
||||||
|
'{"title": "Soda"}'
|
||||||
|
# ---
|
||||||
|
# name: test_update_todo_list_item[api_responses0]
|
||||||
|
tuple(
|
||||||
|
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
|
||||||
|
'PATCH',
|
||||||
|
)
|
||||||
|
# ---
|
||||||
|
# name: test_update_todo_list_item[api_responses0].1
|
||||||
|
'{"title": "Soda", "status": "completed"}'
|
||||||
|
# ---
|
@ -3,11 +3,14 @@
|
|||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
import json
|
import json
|
||||||
from unittest.mock import patch
|
from typing import Any
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
from httplib2 import Response
|
from httplib2 import Response
|
||||||
import pytest
|
import pytest
|
||||||
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.todo import DOMAIN as TODO_DOMAIN
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
@ -22,6 +25,10 @@ LIST_TASK_LIST_RESPONSE = {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
EMPTY_RESPONSE = {}
|
||||||
|
LIST_TASKS_RESPONSE = {
|
||||||
|
"items": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -76,14 +83,14 @@ def mock_api_responses() -> list[dict | list]:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def mock_http_response(api_responses: list[dict | list]) -> None:
|
def mock_http_response(api_responses: list[dict | list]) -> Mock:
|
||||||
"""Fixture to fake out http2lib responses."""
|
"""Fixture to fake out http2lib responses."""
|
||||||
responses = [
|
responses = [
|
||||||
(Response({}), bytes(json.dumps(api_response), encoding="utf-8"))
|
(Response({}), bytes(json.dumps(api_response), encoding="utf-8"))
|
||||||
for api_response in api_responses
|
for api_response in api_responses
|
||||||
]
|
]
|
||||||
with patch("httplib2.Http.request", side_effect=responses):
|
with patch("httplib2.Http.request", side_effect=responses) as mock_response:
|
||||||
yield
|
yield mock_response
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@ -138,9 +145,7 @@ async def test_get_items(
|
|||||||
[
|
[
|
||||||
[
|
[
|
||||||
LIST_TASK_LIST_RESPONSE,
|
LIST_TASK_LIST_RESPONSE,
|
||||||
{
|
LIST_TASKS_RESPONSE,
|
||||||
"items": [],
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -163,3 +168,163 @@ async def test_empty_todo_list(
|
|||||||
state = hass.states.get("todo.my_tasks")
|
state = hass.states.get("todo.my_tasks")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "0"
|
assert state.state == "0"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"api_responses",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
LIST_TASK_LIST_RESPONSE,
|
||||||
|
LIST_TASKS_RESPONSE,
|
||||||
|
EMPTY_RESPONSE, # create
|
||||||
|
LIST_TASKS_RESPONSE, # refresh after create
|
||||||
|
]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_create_todo_list_item(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_credentials: None,
|
||||||
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
|
mock_http_response: Mock,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test for creating a To-do Item."""
|
||||||
|
|
||||||
|
assert await integration_setup()
|
||||||
|
|
||||||
|
state = hass.states.get("todo.my_tasks")
|
||||||
|
assert state
|
||||||
|
assert state.state == "0"
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"create_item",
|
||||||
|
{"summary": "Soda"},
|
||||||
|
target={"entity_id": "todo.my_tasks"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert len(mock_http_response.call_args_list) == 4
|
||||||
|
call = mock_http_response.call_args_list[2]
|
||||||
|
assert call
|
||||||
|
assert call.args == snapshot
|
||||||
|
assert call.kwargs.get("body") == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"api_responses",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
LIST_TASK_LIST_RESPONSE,
|
||||||
|
LIST_TASKS_RESPONSE,
|
||||||
|
EMPTY_RESPONSE, # update
|
||||||
|
LIST_TASKS_RESPONSE, # refresh after update
|
||||||
|
]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_update_todo_list_item(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_credentials: None,
|
||||||
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
|
mock_http_response: Any,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test for updating a To-do Item."""
|
||||||
|
|
||||||
|
assert await integration_setup()
|
||||||
|
|
||||||
|
state = hass.states.get("todo.my_tasks")
|
||||||
|
assert state
|
||||||
|
assert state.state == "0"
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"update_item",
|
||||||
|
{"uid": "some-task-id", "summary": "Soda", "status": "completed"},
|
||||||
|
target={"entity_id": "todo.my_tasks"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert len(mock_http_response.call_args_list) == 4
|
||||||
|
call = mock_http_response.call_args_list[2]
|
||||||
|
assert call
|
||||||
|
assert call.args == snapshot
|
||||||
|
assert call.kwargs.get("body") == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"api_responses",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
LIST_TASK_LIST_RESPONSE,
|
||||||
|
LIST_TASKS_RESPONSE,
|
||||||
|
EMPTY_RESPONSE, # update
|
||||||
|
LIST_TASKS_RESPONSE, # refresh after update
|
||||||
|
]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_partial_update_title(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_credentials: None,
|
||||||
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
|
mock_http_response: Any,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test for partial update with title only."""
|
||||||
|
|
||||||
|
assert await integration_setup()
|
||||||
|
|
||||||
|
state = hass.states.get("todo.my_tasks")
|
||||||
|
assert state
|
||||||
|
assert state.state == "0"
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"update_item",
|
||||||
|
{"uid": "some-task-id", "summary": "Soda"},
|
||||||
|
target={"entity_id": "todo.my_tasks"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert len(mock_http_response.call_args_list) == 4
|
||||||
|
call = mock_http_response.call_args_list[2]
|
||||||
|
assert call
|
||||||
|
assert call.args == snapshot
|
||||||
|
assert call.kwargs.get("body") == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"api_responses",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
LIST_TASK_LIST_RESPONSE,
|
||||||
|
LIST_TASKS_RESPONSE,
|
||||||
|
EMPTY_RESPONSE, # update
|
||||||
|
LIST_TASKS_RESPONSE, # refresh after update
|
||||||
|
]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_partial_update_status(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_credentials: None,
|
||||||
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
|
mock_http_response: Any,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test for partial update with status only."""
|
||||||
|
|
||||||
|
assert await integration_setup()
|
||||||
|
|
||||||
|
state = hass.states.get("todo.my_tasks")
|
||||||
|
assert state
|
||||||
|
assert state.state == "0"
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"update_item",
|
||||||
|
{"uid": "some-task-id", "status": "needs_action"},
|
||||||
|
target={"entity_id": "todo.my_tasks"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert len(mock_http_response.call_args_list) == 4
|
||||||
|
call = mock_http_response.call_args_list[2]
|
||||||
|
assert call
|
||||||
|
assert call.args == snapshot
|
||||||
|
assert call.kwargs.get("body") == snapshot
|
||||||
|
Loading…
x
Reference in New Issue
Block a user