diff --git a/README.md b/README.md index c401232..38a9220 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/custom_components/google_keep_sync/__init__.py b/custom_components/google_keep_sync/__init__.py index 61b4b73..0ceae5b 100644 --- a/custom_components/google_keep_sync/__init__.py +++ b/custom_components/google_keep_sync/__init__.py @@ -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 @@ -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 @@ -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 diff --git a/custom_components/google_keep_sync/coordinator.py b/custom_components/google_keep_sync/coordinator.py index a2754d3..bae658d 100644 --- a/custom_components/google_keep_sync/coordinator.py +++ b/custom_components/google_keep_sync/coordinator.py @@ -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 @@ -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__( diff --git a/custom_components/google_keep_sync/services.yaml b/custom_components/google_keep_sync/services.yaml new file mode 100644 index 0000000..366dd8a --- /dev/null +++ b/custom_components/google_keep_sync/services.yaml @@ -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. diff --git a/tests/test_init.py b/tests/test_init.py index a942ec6..58d340b 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -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 @@ -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()