Improve rate limit handling in Habitica integration (#121763)

* Adjustments to requests and update interval due to rate limiting

* Use debounced refresh for to-do lists

* Use debounced refresh in switch and buttons

* Request refresh only if a to-do was changed

* Update task order provisionally in the coordinator
This commit is contained in:
Mr. Bubbles 2024-08-16 11:41:04 +02:00 committed by GitHub
parent 66a8733333
commit 4b62dcfd19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 50 additions and 20 deletions

View File

@ -113,7 +113,7 @@ class HabiticaButton(HabiticaBase, ButtonEntity):
translation_key="service_call_exception", translation_key="service_call_exception",
) from e ) from e
else: else:
await self.coordinator.async_refresh() await self.coordinator.async_request_refresh()
@property @property
def available(self) -> bool: def available(self) -> bool:

View File

@ -15,6 +15,7 @@ from habitipy.aio import HabitipyAsync
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN from .const import DOMAIN
@ -41,7 +42,13 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
hass, hass,
_LOGGER, _LOGGER,
name=DOMAIN, name=DOMAIN,
update_interval=timedelta(seconds=30), update_interval=timedelta(seconds=60),
request_refresh_debouncer=Debouncer(
hass,
_LOGGER,
cooldown=5,
immediate=False,
),
) )
self.api = habitipy self.api = habitipy
@ -51,6 +58,9 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
tasks_response = await self.api.tasks.user.get() tasks_response = await self.api.tasks.user.get()
tasks_response.extend(await self.api.tasks.user.get(type="completedTodos")) tasks_response.extend(await self.api.tasks.user.get(type="completedTodos"))
except ClientResponseError as error: except ClientResponseError as error:
if error.status == HTTPStatus.TOO_MANY_REQUESTS:
_LOGGER.debug("Currently rate limited, skipping update")
return self.data
raise UpdateFailed(f"Error communicating with API: {error}") from error raise UpdateFailed(f"Error communicating with API: {error}") from error
return HabiticaData(user=user_response, tasks=tasks_response) return HabiticaData(user=user_response, tasks=tasks_response)
@ -73,4 +83,4 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
translation_key="service_call_exception", translation_key="service_call_exception",
) from e ) from e
else: else:
await self.async_refresh() await self.async_request_refresh()

View File

@ -93,7 +93,7 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
translation_key=f"delete_{self.entity_description.key}_failed", translation_key=f"delete_{self.entity_description.key}_failed",
) from e ) from e
await self.coordinator.async_refresh() await self.coordinator.async_request_refresh()
async def async_move_todo_item( async def async_move_todo_item(
self, uid: str, previous_uid: str | None = None self, uid: str, previous_uid: str | None = None
@ -121,9 +121,22 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
translation_key=f"move_{self.entity_description.key}_item_failed", translation_key=f"move_{self.entity_description.key}_item_failed",
translation_placeholders={"pos": str(pos)}, translation_placeholders={"pos": str(pos)},
) from e ) from e
else:
# move tasks in the coordinator until we have fresh data
tasks = self.coordinator.data.tasks
new_pos = (
tasks.index(next(task for task in tasks if task["id"] == previous_uid))
+ 1
if previous_uid
else 0
)
old_pos = tasks.index(next(task for task in tasks if task["id"] == uid))
tasks.insert(new_pos, tasks.pop(old_pos))
await self.coordinator.async_request_refresh()
async def async_update_todo_item(self, item: TodoItem) -> None: async def async_update_todo_item(self, item: TodoItem) -> None:
"""Update a Habitica todo.""" """Update a Habitica todo."""
refresh_required = False
current_item = next( current_item = next(
(task for task in (self.todo_items or []) if task.uid == item.uid), (task for task in (self.todo_items or []) if task.uid == item.uid),
None, None,
@ -132,7 +145,6 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
if TYPE_CHECKING: if TYPE_CHECKING:
assert item.uid assert item.uid
assert current_item assert current_item
assert item.due
if ( if (
self.entity_description.key is HabiticaTodoList.TODOS self.entity_description.key is HabiticaTodoList.TODOS
@ -142,18 +154,24 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
else: else:
date = None date = None
try: if (
await self.coordinator.api.tasks[item.uid].put( item.summary != current_item.summary
text=item.summary, or item.description != current_item.description
notes=item.description or "", or item.due != current_item.due
date=date, ):
) try:
except ClientResponseError as e: await self.coordinator.api.tasks[item.uid].put(
raise ServiceValidationError( text=item.summary,
translation_domain=DOMAIN, notes=item.description or "",
translation_key=f"update_{self.entity_description.key}_item_failed", date=date,
translation_placeholders={"name": item.summary or ""}, )
) from e refresh_required = True
except ClientResponseError as e:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key=f"update_{self.entity_description.key}_item_failed",
translation_placeholders={"name": item.summary or ""},
) from e
try: try:
# Score up or down if item status changed # Score up or down if item status changed
@ -164,6 +182,7 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
score_result = ( score_result = (
await self.coordinator.api.tasks[item.uid].score["up"].post() await self.coordinator.api.tasks[item.uid].score["up"].post()
) )
refresh_required = True
elif ( elif (
current_item.status is TodoItemStatus.COMPLETED current_item.status is TodoItemStatus.COMPLETED
and item.status == TodoItemStatus.NEEDS_ACTION and item.status == TodoItemStatus.NEEDS_ACTION
@ -171,6 +190,7 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
score_result = ( score_result = (
await self.coordinator.api.tasks[item.uid].score["down"].post() await self.coordinator.api.tasks[item.uid].score["down"].post()
) )
refresh_required = True
else: else:
score_result = None score_result = None
@ -189,8 +209,8 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
persistent_notification.async_create( persistent_notification.async_create(
self.hass, message=msg, title="Habitica" self.hass, message=msg, title="Habitica"
) )
if refresh_required:
await self.coordinator.async_refresh() await self.coordinator.async_request_refresh()
class HabiticaTodosListEntity(BaseHabiticaListEntity): class HabiticaTodosListEntity(BaseHabiticaListEntity):
@ -254,7 +274,7 @@ class HabiticaTodosListEntity(BaseHabiticaListEntity):
translation_placeholders={"name": item.summary or ""}, translation_placeholders={"name": item.summary or ""},
) from e ) from e
await self.coordinator.async_refresh() await self.coordinator.async_request_refresh()
class HabiticaDailiesListEntity(BaseHabiticaListEntity): class HabiticaDailiesListEntity(BaseHabiticaListEntity):