Skip to content

Commit

Permalink
Add a request_sync service (#23)
Browse files Browse the repository at this point in the history
* Initial commit

* Remove commented out code

* Fix issue with automatic list sort overwriting remote changes

* Add missing test cases

* Fix broken link

* Fix broken named anchor

* Remove unnecessary section

* Extract call from async_setup_entry

* Add request_sync tests

* Update documentation for request_sync

* Log successful sync as info, not warning

* Fix broken test
  • Loading branch information
watkins-matt committed Mar 21, 2024
1 parent 1d2a622 commit 4f2c70d
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 4 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,28 @@ use the built-in services to add, remove and update items from your synchronized
- `todo.add_item`
- `todo.remove_item`
- `todo.update_item`
- `google_keep_sync.request_sync`

#### google_keep_sync.request_sync

This service can be used to trigger a manual sync of all of your lists. This is
helpful because you can use it from an automation or script. While you can use also
use `homeassistant.update_entity` to trigger a sync, that service requires you
to specify a certain entity id, while this service targets all of your lists.

There is a built in cooldown to ensure that this service is not called
too frequently. Instead, it will log a warning if you call it too quickly.

Note that in some cases, the Google Keep Android app does not immediately send
changes you have made to Google's servers. This means that if you call the service
right after making the change on the Android app, it may not pick it up. It is
recommended to add a delay before calling the service if you are trying to capture
changes made on the Android app.

If you are using the Google Keep website or webapp, you can see the sync progress
icon in the top right corner of the screen. Once it has finished
spinning and you see the cloud icon with a checkmark, you can
safely call the service.

## Events

Expand Down
25 changes: 24 additions & 1 deletion custom_components/google_keep_sync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from __future__ import annotations

import logging
from functools import partial

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.util.dt import as_timestamp, utcnow

from .api import GoogleKeepAPI
from .const import DOMAIN
Expand All @@ -17,6 +19,23 @@
_LOGGER = logging.getLogger(__name__)


async def async_service_request_sync(coordinator: GoogleKeepSyncCoordinator, call):
"""Handle the request_sync call."""
sync_threshold = 55
last_update_timestamp = as_timestamp(coordinator.last_update_success_time)
seconds_since_update = as_timestamp(utcnow()) - last_update_timestamp

if seconds_since_update > sync_threshold:
_LOGGER.info("Requesting manual sync.")
await coordinator.async_refresh()
else:
time_to_next_allowed_update = round(sync_threshold - seconds_since_update)
_LOGGER.warning(
"Requesting sync too soon after last update."
f" Try again in {time_to_next_allowed_update} seconds."
)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Google Keep Sync from a config entry."""
# Create API instance
Expand All @@ -37,9 +56,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator

# Register the request_sync service
hass.services.async_register(
DOMAIN, "request_sync", partial(async_service_request_sync, coordinator)
)

# Forward the setup to the todo platform
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True


Expand Down
7 changes: 5 additions & 2 deletions custom_components/google_keep_sync/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
from homeassistant.const import EVENT_CALL_SERVICE, Platform
from homeassistant.core import EventOrigin, HomeAssistant
from homeassistant.helpers import entity_registry
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.helpers.update_coordinator import (
TimestampDataUpdateCoordinator,
UpdateFailed,
)

from .api import GoogleKeepAPI
from .const import DOMAIN, SCAN_INTERVAL
Expand All @@ -19,7 +22,7 @@
TodoItemData = namedtuple("TodoItemData", ["item", "entity_id"])


class GoogleKeepSyncCoordinator(DataUpdateCoordinator[list[GKeepList]]):
class GoogleKeepSyncCoordinator(TimestampDataUpdateCoordinator[list[GKeepList]]):
"""Coordinator for updating task data from Google Keep."""

def __init__(
Expand Down
2 changes: 2 additions & 0 deletions custom_components/google_keep_sync/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
request_sync:
description: Requests a sync of Google Keep notes. This will update Home Assistant with the latest notes from Google Keep. Note that calling this too frequently will result in a cooldown period, where further request_sync calls will be ignored until the cooldown period has expired.
44 changes: 43 additions & 1 deletion tests/test_init.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
"""Test the Google Keep Sync setup entry."""

from datetime import timedelta
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from homeassistant.core import HomeAssistant
from homeassistant.util.dt import utcnow

from custom_components.google_keep_sync import async_setup_entry, async_unload_entry
from custom_components.google_keep_sync import (
async_service_request_sync,
async_setup_entry,
async_unload_entry,
)
from custom_components.google_keep_sync.const import DOMAIN as GOOGLE_KEEP_DOMAIN


Expand Down Expand Up @@ -59,3 +65,39 @@ async def test_async_unload_entry(hass: HomeAssistant, mock_api, mock_config_ent
assert await async_unload_entry(hass, mock_config_entry)
assert not hass.data[GOOGLE_KEEP_DOMAIN].get(mock_config_entry.entry_id)
await hass.async_block_till_done()


async def test_async_service_request_sync_refresh_called(hass: HomeAssistant, mock_api):
"""Test that async_refresh is called when the sync threshold is exceeded."""
coordinator = AsyncMock()
coordinator.last_update_success_time = utcnow()
coordinator.async_refresh = AsyncMock()

with patch(
"custom_components.google_keep_sync.utcnow",
return_value=coordinator.last_update_success_time + timedelta(seconds=60),
), patch("custom_components.google_keep_sync._LOGGER") as mock_logger:

# Simulate the service call
await async_service_request_sync(coordinator, None)
assert coordinator.async_refresh.called
mock_logger.info.assert_called_with("Requesting manual sync.")


async def test_async_service_request_sync_too_soon_warning(
hass: HomeAssistant, mock_api
):
"""Test that a warning is logged if a sync is requested too soon."""
coordinator = AsyncMock()
coordinator.last_update_success_time = utcnow()
coordinator.async_refresh = AsyncMock()

with patch(
"custom_components.google_keep_sync.utcnow",
return_value=coordinator.last_update_success_time + timedelta(seconds=50),
), patch("custom_components.google_keep_sync._LOGGER") as mock_logger:

# Simulate the service call
await async_service_request_sync(coordinator, None)
assert not coordinator.async_refresh.called
mock_logger.warning.assert_called()

0 comments on commit 4f2c70d

Please sign in to comment.