Add persistent storage
This commit is contained in:
@@ -20,6 +20,7 @@ from .const import (
|
||||
PLATFORMS,
|
||||
)
|
||||
from .coordinator import ImmichAlbumWatcherCoordinator
|
||||
from .storage import ImmichAlbumStorage
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -63,10 +64,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ImmichConfigEntry) -> bo
|
||||
scan_interval=scan_interval,
|
||||
)
|
||||
|
||||
# Create storage for persisting album state across restarts
|
||||
storage = ImmichAlbumStorage(hass, entry.entry_id)
|
||||
await storage.async_load()
|
||||
|
||||
# Store hub reference
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
"hub": entry.runtime_data,
|
||||
"subentries": {},
|
||||
"storage": storage,
|
||||
}
|
||||
|
||||
# Track loaded subentries to detect changes
|
||||
@@ -97,6 +103,7 @@ async def _async_setup_subentry_coordinator(
|
||||
hub_data: ImmichHubData = entry.runtime_data
|
||||
album_id = subentry.data[CONF_ALBUM_ID]
|
||||
album_name = subentry.data.get(CONF_ALBUM_NAME, "Unknown Album")
|
||||
storage: ImmichAlbumStorage = hass.data[DOMAIN][entry.entry_id]["storage"]
|
||||
|
||||
_LOGGER.debug("Setting up coordinator for album: %s (%s)", album_name, album_id)
|
||||
|
||||
@@ -109,8 +116,12 @@ async def _async_setup_subentry_coordinator(
|
||||
album_name=album_name,
|
||||
scan_interval=hub_data.scan_interval,
|
||||
hub_name=hub_data.name,
|
||||
storage=storage,
|
||||
)
|
||||
|
||||
# Load persisted state before first refresh to detect changes during downtime
|
||||
await coordinator.async_load_persisted_state()
|
||||
|
||||
# Fetch initial data
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ from __future__ import annotations
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .storage import ImmichAlbumStorage
|
||||
|
||||
import aiohttp
|
||||
|
||||
@@ -221,6 +224,7 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
|
||||
album_name: str,
|
||||
scan_interval: int,
|
||||
hub_name: str = "Immich",
|
||||
storage: ImmichAlbumStorage | None = None,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
@@ -239,6 +243,8 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
|
||||
self._people_cache: dict[str, str] = {} # person_id -> name
|
||||
self._users_cache: dict[str, str] = {} # user_id -> name
|
||||
self._shared_links: list[SharedLinkInfo] = []
|
||||
self._storage = storage
|
||||
self._persisted_asset_ids: set[str] | None = None
|
||||
|
||||
@property
|
||||
def immich_url(self) -> str:
|
||||
@@ -268,6 +274,23 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
|
||||
"""Force an immediate refresh."""
|
||||
await self.async_request_refresh()
|
||||
|
||||
async def async_load_persisted_state(self) -> None:
|
||||
"""Load persisted asset IDs from storage.
|
||||
|
||||
This should be called before the first refresh to enable
|
||||
detection of changes that occurred during downtime.
|
||||
"""
|
||||
if self._storage:
|
||||
self._persisted_asset_ids = self._storage.get_album_asset_ids(
|
||||
self._album_id
|
||||
)
|
||||
if self._persisted_asset_ids is not None:
|
||||
_LOGGER.debug(
|
||||
"Loaded %d persisted asset IDs for album '%s'",
|
||||
len(self._persisted_asset_ids),
|
||||
self._album_name,
|
||||
)
|
||||
|
||||
async def async_get_recent_assets(self, count: int = 10) -> list[dict[str, Any]]:
|
||||
"""Get recent assets from the album."""
|
||||
if self.data is None:
|
||||
@@ -503,6 +526,47 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
|
||||
album.has_new_assets = change.added_count > 0
|
||||
album.last_change_time = datetime.now()
|
||||
self._fire_events(change, album)
|
||||
elif self._persisted_asset_ids is not None:
|
||||
# First refresh after restart - compare with persisted state
|
||||
added_ids = album.asset_ids - self._persisted_asset_ids
|
||||
removed_ids = self._persisted_asset_ids - album.asset_ids
|
||||
|
||||
if added_ids or removed_ids:
|
||||
change_type = "changed"
|
||||
if added_ids and not removed_ids:
|
||||
change_type = "assets_added"
|
||||
elif removed_ids and not added_ids:
|
||||
change_type = "assets_removed"
|
||||
|
||||
added_assets = [
|
||||
album.assets[aid]
|
||||
for aid in added_ids
|
||||
if aid in album.assets
|
||||
]
|
||||
|
||||
change = AlbumChange(
|
||||
album_id=album.id,
|
||||
album_name=album.name,
|
||||
change_type=change_type,
|
||||
added_count=len(added_ids),
|
||||
removed_count=len(removed_ids),
|
||||
added_assets=added_assets,
|
||||
removed_asset_ids=list(removed_ids),
|
||||
)
|
||||
album.has_new_assets = change.added_count > 0
|
||||
album.last_change_time = datetime.now()
|
||||
self._fire_events(change, album)
|
||||
_LOGGER.info(
|
||||
"Detected changes during downtime for album '%s': +%d -%d",
|
||||
album.name,
|
||||
len(added_ids),
|
||||
len(removed_ids),
|
||||
)
|
||||
else:
|
||||
album.has_new_assets = False
|
||||
|
||||
# Clear persisted state after first comparison
|
||||
self._persisted_asset_ids = None
|
||||
else:
|
||||
album.has_new_assets = False
|
||||
|
||||
@@ -517,6 +581,12 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
|
||||
# Update previous state
|
||||
self._previous_state = album
|
||||
|
||||
# Persist current state for recovery after restart
|
||||
if self._storage:
|
||||
await self._storage.async_save_album_state(
|
||||
self._album_id, album.asset_ids
|
||||
)
|
||||
|
||||
return album
|
||||
|
||||
except aiohttp.ClientError as err:
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"issue_tracker": "https://github.com/DolgolyovAlexei/haos-hacs-immich-album-watcher/issues",
|
||||
"requirements": [],
|
||||
"version": "1.2.0"
|
||||
"version": "1.3.0"
|
||||
}
|
||||
|
||||
65
custom_components/immich_album_watcher/storage.py
Normal file
65
custom_components/immich_album_watcher/storage.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""Storage helpers for Immich Album Watcher."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY_PREFIX = "immich_album_watcher"
|
||||
|
||||
|
||||
class ImmichAlbumStorage:
|
||||
"""Handles persistence of album state across restarts."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry_id: str) -> None:
|
||||
"""Initialize the storage."""
|
||||
self._store: Store[dict[str, Any]] = Store(
|
||||
hass, STORAGE_VERSION, f"{STORAGE_KEY_PREFIX}.{entry_id}"
|
||||
)
|
||||
self._data: dict[str, Any] | None = None
|
||||
|
||||
async def async_load(self) -> dict[str, Any]:
|
||||
"""Load data from storage."""
|
||||
self._data = await self._store.async_load() or {"albums": {}}
|
||||
_LOGGER.debug("Loaded storage data with %d albums", len(self._data.get("albums", {})))
|
||||
return self._data
|
||||
|
||||
async def async_save_album_state(self, album_id: str, asset_ids: set[str]) -> None:
|
||||
"""Save album asset IDs to storage."""
|
||||
if self._data is None:
|
||||
self._data = {"albums": {}}
|
||||
|
||||
self._data["albums"][album_id] = {
|
||||
"asset_ids": list(asset_ids),
|
||||
"last_updated": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
await self._store.async_save(self._data)
|
||||
|
||||
def get_album_asset_ids(self, album_id: str) -> set[str] | None:
|
||||
"""Get persisted asset IDs for an album.
|
||||
|
||||
Returns None if no persisted state exists for the album.
|
||||
"""
|
||||
if self._data and "albums" in self._data:
|
||||
album_data = self._data["albums"].get(album_id)
|
||||
if album_data:
|
||||
return set(album_data.get("asset_ids", []))
|
||||
return None
|
||||
|
||||
async def async_remove_album(self, album_id: str) -> None:
|
||||
"""Remove an album from storage."""
|
||||
if self._data and "albums" in self._data:
|
||||
self._data["albums"].pop(album_id, None)
|
||||
await self._store.async_save(self._data)
|
||||
|
||||
async def async_remove(self) -> None:
|
||||
"""Remove all storage data."""
|
||||
await self._store.async_remove()
|
||||
self._data = None
|
||||
Reference in New Issue
Block a user