Some checks failed
Validate / Hassfest (push) Has been cancelled
- Asset cache now validates entries by comparing stored thumbhash with current Immich thumbhash instead of using TTL expiration. This makes cache invalidation precise (only when content actually changes) and eliminates unnecessary re-uploads. URL-based cache retains TTL for non-Immich URLs. - Add TELEGRAM_MAX_VIDEO_SIZE (50 MB) check to skip oversized videos in both single-video and media-group paths, preventing entire groups from failing. - Split media groups into sub-groups by cumulative upload size to ensure each sendMediaGroup request stays under Telegram's 50 MB upload limit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
205 lines
6.7 KiB
Python
205 lines
6.7 KiB
Python
"""Immich Album Watcher integration for Home Assistant."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
|
|
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
|
|
from homeassistant.core import HomeAssistant
|
|
|
|
from .const import (
|
|
CONF_ALBUM_ID,
|
|
CONF_ALBUM_NAME,
|
|
CONF_API_KEY,
|
|
CONF_HUB_NAME,
|
|
CONF_IMMICH_URL,
|
|
CONF_SCAN_INTERVAL,
|
|
CONF_TELEGRAM_CACHE_TTL,
|
|
DEFAULT_SCAN_INTERVAL,
|
|
DEFAULT_TELEGRAM_CACHE_TTL,
|
|
DOMAIN,
|
|
PLATFORMS,
|
|
)
|
|
from .coordinator import ImmichAlbumWatcherCoordinator
|
|
from .storage import ImmichAlbumStorage, TelegramFileCache
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class ImmichHubData:
|
|
"""Data for the Immich hub."""
|
|
|
|
name: str
|
|
url: str
|
|
api_key: str
|
|
scan_interval: int
|
|
telegram_cache_ttl: int
|
|
|
|
|
|
@dataclass
|
|
class ImmichAlbumRuntimeData:
|
|
"""Runtime data for an album subentry."""
|
|
|
|
coordinator: ImmichAlbumWatcherCoordinator
|
|
album_id: str
|
|
album_name: str
|
|
|
|
|
|
type ImmichConfigEntry = ConfigEntry[ImmichHubData]
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ImmichConfigEntry) -> bool:
|
|
"""Set up Immich Album Watcher hub from a config entry."""
|
|
hass.data.setdefault(DOMAIN, {})
|
|
|
|
hub_name = entry.data.get(CONF_HUB_NAME, "Immich")
|
|
url = entry.data[CONF_IMMICH_URL]
|
|
api_key = entry.data[CONF_API_KEY]
|
|
scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
|
telegram_cache_ttl = entry.options.get(CONF_TELEGRAM_CACHE_TTL, DEFAULT_TELEGRAM_CACHE_TTL)
|
|
|
|
# Store hub data
|
|
entry.runtime_data = ImmichHubData(
|
|
name=hub_name,
|
|
url=url,
|
|
api_key=api_key,
|
|
scan_interval=scan_interval,
|
|
telegram_cache_ttl=telegram_cache_ttl,
|
|
)
|
|
|
|
# Create storage for persisting album state across restarts
|
|
storage = ImmichAlbumStorage(hass, entry.entry_id)
|
|
await storage.async_load()
|
|
|
|
# Create and load Telegram file caches once per hub (shared across all albums)
|
|
# TTL is in hours from config, convert to seconds
|
|
cache_ttl_seconds = telegram_cache_ttl * 60 * 60
|
|
# URL-based cache for non-Immich URLs or URLs without extractable asset IDs
|
|
telegram_cache = TelegramFileCache(hass, entry.entry_id, ttl_seconds=cache_ttl_seconds)
|
|
await telegram_cache.async_load()
|
|
# Asset ID-based cache for Immich URLs — uses thumbhash validation instead of TTL
|
|
telegram_asset_cache = TelegramFileCache(
|
|
hass, f"{entry.entry_id}_assets", use_thumbhash=True
|
|
)
|
|
await telegram_asset_cache.async_load()
|
|
|
|
# Store hub reference
|
|
hass.data[DOMAIN][entry.entry_id] = {
|
|
"hub": entry.runtime_data,
|
|
"subentries": {},
|
|
"storage": storage,
|
|
"telegram_cache": telegram_cache,
|
|
"telegram_asset_cache": telegram_asset_cache,
|
|
}
|
|
|
|
# Track loaded subentries to detect changes
|
|
hass.data[DOMAIN][entry.entry_id]["loaded_subentries"] = set(entry.subentries.keys())
|
|
|
|
# Set up coordinators for all subentries (albums)
|
|
for subentry_id, subentry in entry.subentries.items():
|
|
await _async_setup_subentry_coordinator(hass, entry, subentry)
|
|
|
|
# Forward platform setup once - platforms will iterate through subentries
|
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
|
|
# Register update listener for options and subentry changes
|
|
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
|
|
|
_LOGGER.info(
|
|
"Immich Album Watcher hub set up successfully with %d albums",
|
|
len(entry.subentries),
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
async def _async_setup_subentry_coordinator(
|
|
hass: HomeAssistant, entry: ImmichConfigEntry, subentry: ConfigSubentry
|
|
) -> None:
|
|
"""Set up coordinator for an album subentry."""
|
|
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"]
|
|
telegram_cache: TelegramFileCache = hass.data[DOMAIN][entry.entry_id]["telegram_cache"]
|
|
telegram_asset_cache: TelegramFileCache = hass.data[DOMAIN][entry.entry_id]["telegram_asset_cache"]
|
|
|
|
_LOGGER.debug("Setting up coordinator for album: %s (%s)", album_name, album_id)
|
|
|
|
# Create coordinator for this album
|
|
coordinator = ImmichAlbumWatcherCoordinator(
|
|
hass,
|
|
url=hub_data.url,
|
|
api_key=hub_data.api_key,
|
|
album_id=album_id,
|
|
album_name=album_name,
|
|
scan_interval=hub_data.scan_interval,
|
|
hub_name=hub_data.name,
|
|
storage=storage,
|
|
telegram_cache=telegram_cache,
|
|
telegram_asset_cache=telegram_asset_cache,
|
|
)
|
|
|
|
# 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()
|
|
|
|
# Store subentry runtime data
|
|
subentry_data = ImmichAlbumRuntimeData(
|
|
coordinator=coordinator,
|
|
album_id=album_id,
|
|
album_name=album_name,
|
|
)
|
|
hass.data[DOMAIN][entry.entry_id]["subentries"][subentry.subentry_id] = subentry_data
|
|
|
|
_LOGGER.info("Coordinator for album '%s' set up successfully", album_name)
|
|
|
|
|
|
async def _async_update_listener(
|
|
hass: HomeAssistant, entry: ImmichConfigEntry
|
|
) -> None:
|
|
"""Handle config entry updates (options or subentry changes)."""
|
|
entry_data = hass.data[DOMAIN][entry.entry_id]
|
|
loaded_subentries = entry_data.get("loaded_subentries", set())
|
|
current_subentries = set(entry.subentries.keys())
|
|
|
|
# Check if subentries changed
|
|
if loaded_subentries != current_subentries:
|
|
_LOGGER.info(
|
|
"Subentries changed (loaded: %d, current: %d), reloading entry",
|
|
len(loaded_subentries),
|
|
len(current_subentries),
|
|
)
|
|
await hass.config_entries.async_reload(entry.entry_id)
|
|
return
|
|
|
|
# Handle options-only update (scan interval change)
|
|
new_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
|
|
|
# Update hub data
|
|
entry.runtime_data.scan_interval = new_interval
|
|
|
|
# Update all subentry coordinators
|
|
subentries_data = entry_data["subentries"]
|
|
for subentry_data in subentries_data.values():
|
|
subentry_data.coordinator.update_scan_interval(new_interval)
|
|
|
|
_LOGGER.info("Updated scan interval to %d seconds", new_interval)
|
|
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ImmichConfigEntry) -> bool:
|
|
"""Unload a config entry."""
|
|
# Unload all platforms
|
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
|
|
if unload_ok:
|
|
# Clean up hub data
|
|
hass.data[DOMAIN].pop(entry.entry_id, None)
|
|
_LOGGER.info("Immich Album Watcher hub unloaded")
|
|
|
|
return unload_ok
|