Improve Telegram error handling and unify asset data structure
All checks were successful
Validate / Hassfest (push) Successful in 3s

- Remove photo downscaling logic in favor of cleaner error handling
- Add intelligent Telegram API error logging with diagnostics and suggestions
- Define Telegram photo limits as global constants (TELEGRAM_MAX_PHOTO_SIZE, TELEGRAM_MAX_DIMENSION_SUM)
- Add photo_url support for image assets (matching video_url for videos)
- Unify asset detail building with shared _build_asset_detail() helper method
- Enhance get_assets service to return complete asset data matching events
- Simplify attribute naming by removing redundant asset_ prefix from values

BREAKING CHANGE: Asset attribute keys changed from "asset_type", "asset_filename"
to simpler "type", "filename" for consistency and cleaner JSON responses

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 23:40:19 +03:00
parent bbcd97e1ac
commit 8714685d5e
7 changed files with 373 additions and 176 deletions

View File

@@ -4,6 +4,8 @@
A Home Assistant custom integration that monitors [Immich](https://immich.app/) photo/video library albums for changes and exposes them as Home Assistant entities with event-firing capabilities. A Home Assistant custom integration that monitors [Immich](https://immich.app/) photo/video library albums for changes and exposes them as Home Assistant entities with event-firing capabilities.
> **Tip:** For the best experience, use this integration with the [Immich Album Watcher Blueprint](https://github.com/DolgolyovAlexei/haos-blueprints/blob/main/Common/Immich%20Album%20Watcher.yaml) to easily create automations for album change notifications.
## Features ## Features
- **Album Monitoring** - Watch selected Immich albums for asset additions and removals - **Album Monitoring** - Watch selected Immich albums for asset additions and removals
@@ -31,7 +33,7 @@ A Home Assistant custom integration that monitors [Immich](https://immich.app/)
- Detected people in the asset - Detected people in the asset
- **Services** - Custom service calls: - **Services** - Custom service calls:
- `immich_album_watcher.refresh` - Force immediate data refresh - `immich_album_watcher.refresh` - Force immediate data refresh
- `immich_album_watcher.get_recent_assets` - Get recent assets from an album - `immich_album_watcher.get_assets` - Get assets from an album with filtering and ordering
- `immich_album_watcher.send_telegram_notification` - Send text, photo, video, or media group to Telegram - `immich_album_watcher.send_telegram_notification` - Send text, photo, video, or media group to Telegram
- **Share Link Management** - Button entities to create and delete share links: - **Share Link Management** - Button entities to create and delete share links:
- Create/delete public (unprotected) share links - Create/delete public (unprotected) share links
@@ -60,8 +62,6 @@ A Home Assistant custom integration that monitors [Immich](https://immich.app/)
3. Restart Home Assistant 3. Restart Home Assistant
4. Add the integration via **Settings****Devices & Services****Add Integration** 4. Add the integration via **Settings****Devices & Services****Add Integration**
> **Tip:** For the best experience, use this integration with the [Immich Album Watcher Blueprint](https://github.com/DolgolyovAlexei/haos-blueprints/blob/main/Common/Immich%20Album%20Watcher.yaml) to easily create automations for album change notifications.
## Configuration ## Configuration
| Option | Description | Default | | Option | Description | Default |
@@ -103,16 +103,59 @@ Force an immediate refresh of all album data:
service: immich_album_watcher.refresh service: immich_album_watcher.refresh
``` ```
### Get Recent Assets ### Get Assets
Get the most recent assets from a specific album (returns response data): Get assets from a specific album with optional filtering and ordering (returns response data):
```yaml ```yaml
service: immich_album_watcher.get_recent_assets service: immich_album_watcher.get_assets
target:
entity_id: sensor.album_name_asset_count
data:
count: 10 # Maximum number of assets (1-100)
filter: "favorite" # Options: "none", "favorite", "rating"
filter_min_rating: 4 # Min rating (1-5), used when filter="rating"
order: "descending" # Options: "ascending", "descending", "random"
```
**Parameters:**
- `count` (optional, default: 10): Maximum number of assets to return (1-100)
- `filter` (optional, default: "none"): Filter type
- `"none"`: No filtering, return all assets
- `"favorite"`: Return only favorite assets
- `"rating"`: Return assets with rating >= `filter_min_rating`
- `filter_min_rating` (optional, default: 1): Minimum rating (1-5 stars), used when `filter="rating"`
- `order` (optional, default: "descending"): Sort order by creation date
- `"ascending"`: Oldest first
- `"descending"`: Newest first
- `"random"`: Random order
**Examples:**
Get 5 most recent favorite assets:
```yaml
service: immich_album_watcher.get_assets
target:
entity_id: sensor.album_name_asset_count
data:
count: 5
filter: "favorite"
order: "descending"
```
Get 10 random assets rated 3 stars or higher:
```yaml
service: immich_album_watcher.get_assets
target: target:
entity_id: sensor.album_name_asset_count entity_id: sensor.album_name_asset_count
data: data:
count: 10 count: 10
filter: "rating"
filter_min_rating: 3
order: "random"
``` ```
### Send Telegram Notification ### Send Telegram Notification

View File

@@ -57,17 +57,17 @@ ATTR_OLD_NAME: Final = "old_name"
ATTR_NEW_NAME: Final = "new_name" ATTR_NEW_NAME: Final = "new_name"
ATTR_OLD_SHARED: Final = "old_shared" ATTR_OLD_SHARED: Final = "old_shared"
ATTR_NEW_SHARED: Final = "new_shared" ATTR_NEW_SHARED: Final = "new_shared"
ATTR_ASSET_TYPE: Final = "asset_type" ATTR_ASSET_TYPE: Final = "type"
ATTR_ASSET_FILENAME: Final = "asset_filename" ATTR_ASSET_FILENAME: Final = "filename"
ATTR_ASSET_CREATED: Final = "asset_created" ATTR_ASSET_CREATED: Final = "created_at"
ATTR_ASSET_OWNER: Final = "asset_owner" ATTR_ASSET_OWNER: Final = "owner"
ATTR_ASSET_OWNER_ID: Final = "asset_owner_id" ATTR_ASSET_OWNER_ID: Final = "owner_id"
ATTR_ASSET_URL: Final = "asset_url" ATTR_ASSET_URL: Final = "url"
ATTR_ASSET_DOWNLOAD_URL: Final = "asset_download_url" ATTR_ASSET_DOWNLOAD_URL: Final = "download_url"
ATTR_ASSET_PLAYBACK_URL: Final = "asset_playback_url" ATTR_ASSET_PLAYBACK_URL: Final = "playback_url"
ATTR_ASSET_DESCRIPTION: Final = "asset_description" ATTR_ASSET_DESCRIPTION: Final = "description"
ATTR_ASSET_IS_FAVORITE: Final = "asset_is_favorite" ATTR_ASSET_IS_FAVORITE: Final = "is_favorite"
ATTR_ASSET_RATING: Final = "asset_rating" ATTR_ASSET_RATING: Final = "rating"
# Asset types # Asset types
ASSET_TYPE_IMAGE: Final = "IMAGE" ASSET_TYPE_IMAGE: Final = "IMAGE"
@@ -78,5 +78,5 @@ PLATFORMS: Final = ["sensor", "binary_sensor", "camera", "text", "button"]
# Services # Services
SERVICE_REFRESH: Final = "refresh" SERVICE_REFRESH: Final = "refresh"
SERVICE_GET_RECENT_ASSETS: Final = "get_recent_assets" SERVICE_GET_ASSETS: Final = "get_assets"
SERVICE_SEND_TELEGRAM_NOTIFICATION: Final = "send_telegram_notification" SERVICE_SEND_TELEGRAM_NOTIFICATION: Final = "send_telegram_notification"

View File

@@ -45,6 +45,7 @@ from .const import (
ATTR_OLD_SHARED, ATTR_OLD_SHARED,
ATTR_NEW_SHARED, ATTR_NEW_SHARED,
ATTR_SHARED, ATTR_SHARED,
ATTR_THUMBNAIL_URL,
DOMAIN, DOMAIN,
EVENT_ALBUM_CHANGED, EVENT_ALBUM_CHANGED,
EVENT_ASSETS_ADDED, EVENT_ASSETS_ADDED,
@@ -313,35 +314,52 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
self._album_name, self._album_name,
) )
async def async_get_recent_assets(self, count: int = 10) -> list[dict[str, Any]]: async def async_get_assets(
"""Get recent assets from the album.""" self,
count: int = 10,
filter: str = "none",
filter_min_rating: int = 1,
order: str = "descending",
) -> list[dict[str, Any]]:
"""Get assets from the album with optional filtering and ordering.
Args:
count: Maximum number of assets to return (1-100)
filter: Filter type - 'none', 'favorite', or 'rating'
filter_min_rating: Minimum rating for assets (1-5), used when filter='rating'
order: Sort order - 'ascending', 'descending', or 'random'
Returns:
List of asset data dictionaries
"""
if self.data is None: if self.data is None:
return [] return []
# Sort assets by created_at descending # Start with all assets
sorted_assets = sorted( assets = list(self.data.assets.values())
self.data.assets.values(),
key=lambda a: a.created_at,
reverse=True,
)[:count]
# Apply filtering
if filter == "favorite":
assets = [a for a in assets if a.is_favorite]
elif filter == "rating":
assets = [a for a in assets if a.rating is not None and a.rating >= filter_min_rating]
# Apply ordering
if order == "random":
import random
random.shuffle(assets)
elif order == "ascending":
assets = sorted(assets, key=lambda a: a.created_at, reverse=False)
else: # descending (default)
assets = sorted(assets, key=lambda a: a.created_at, reverse=True)
# Limit count
assets = assets[:count]
# Build result with all available asset data (matching event data)
result = [] result = []
for asset in sorted_assets: for asset in assets:
asset_data = { asset_data = self._build_asset_detail(asset, include_thumbnail=True)
"id": asset.id,
"type": asset.type,
"filename": asset.filename,
"created_at": asset.created_at,
"description": asset.description,
"people": asset.people,
"is_favorite": asset.is_favorite,
"rating": asset.rating,
"thumbnail_url": f"{self._url}/api/assets/{asset.id}/thumbnail",
}
if asset.type == ASSET_TYPE_VIDEO:
video_url = self._get_asset_video_url(asset.id)
if video_url:
asset_data["video_url"] = video_url
result.append(asset_data) result.append(asset_data)
return result return result
@@ -513,6 +531,68 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
return f"{self._url}/api/assets/{asset_id}/video/playback?key={non_expired[0].key}" return f"{self._url}/api/assets/{asset_id}/video/playback?key={non_expired[0].key}"
return None return None
def _get_asset_photo_url(self, asset_id: str) -> str | None:
"""Get the transcoded/preview URL for a photo asset."""
accessible_links = self._get_accessible_links()
if accessible_links:
return f"{self._url}/api/assets/{asset_id}/original?key={accessible_links[0].key}"
non_expired = [link for link in self._shared_links if not link.is_expired]
if non_expired:
return f"{self._url}/api/assets/{asset_id}/original?key={non_expired[0].key}"
return None
def _build_asset_detail(
self, asset: AssetInfo, include_thumbnail: bool = True
) -> dict[str, Any]:
"""Build asset detail dictionary with all available data.
Args:
asset: AssetInfo object
include_thumbnail: If True, include thumbnail_url
Returns:
Dictionary with asset details (using ATTR_* constants for consistency)
"""
# Base asset data
asset_detail = {
"id": asset.id,
ATTR_ASSET_TYPE: asset.type,
ATTR_ASSET_FILENAME: asset.filename,
ATTR_ASSET_CREATED: asset.created_at,
ATTR_ASSET_OWNER: asset.owner_name,
ATTR_ASSET_OWNER_ID: asset.owner_id,
ATTR_ASSET_DESCRIPTION: asset.description,
ATTR_PEOPLE: asset.people,
ATTR_ASSET_IS_FAVORITE: asset.is_favorite,
ATTR_ASSET_RATING: asset.rating,
}
# Add thumbnail URL if requested
if include_thumbnail:
asset_detail[ATTR_THUMBNAIL_URL] = f"{self._url}/api/assets/{asset.id}/thumbnail"
# Add public viewer URL (web page)
asset_url = self._get_asset_public_url(asset.id)
if asset_url:
asset_detail[ATTR_ASSET_URL] = asset_url
# Add download URL (direct media file)
asset_download_url = self._get_asset_download_url(asset.id)
if asset_download_url:
asset_detail[ATTR_ASSET_DOWNLOAD_URL] = asset_download_url
# Add type-specific URLs
if asset.type == ASSET_TYPE_VIDEO:
asset_video_url = self._get_asset_video_url(asset.id)
if asset_video_url:
asset_detail[ATTR_ASSET_PLAYBACK_URL] = asset_video_url
elif asset.type == ASSET_TYPE_IMAGE:
asset_photo_url = self._get_asset_photo_url(asset.id)
if asset_photo_url:
asset_detail["photo_url"] = asset_photo_url # TODO: Add ATTR_ASSET_PHOTO_URL constant
return asset_detail
async def _async_update_data(self) -> AlbumData | None: async def _async_update_data(self) -> AlbumData | None:
"""Fetch data from Immich API.""" """Fetch data from Immich API."""
if self._session is None: if self._session is None:
@@ -673,28 +753,7 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
"""Fire Home Assistant events for album changes.""" """Fire Home Assistant events for album changes."""
added_assets_detail = [] added_assets_detail = []
for asset in change.added_assets: for asset in change.added_assets:
asset_detail = { asset_detail = self._build_asset_detail(asset, include_thumbnail=False)
"id": asset.id,
ATTR_ASSET_TYPE: asset.type,
ATTR_ASSET_FILENAME: asset.filename,
ATTR_ASSET_CREATED: asset.created_at,
ATTR_ASSET_OWNER: asset.owner_name,
ATTR_ASSET_OWNER_ID: asset.owner_id,
ATTR_ASSET_DESCRIPTION: asset.description,
ATTR_PEOPLE: asset.people,
ATTR_ASSET_IS_FAVORITE: asset.is_favorite,
ATTR_ASSET_RATING: asset.rating,
}
asset_url = self._get_asset_public_url(asset.id)
if asset_url:
asset_detail[ATTR_ASSET_URL] = asset_url
asset_download_url = self._get_asset_download_url(asset.id)
if asset_download_url:
asset_detail[ATTR_ASSET_DOWNLOAD_URL] = asset_download_url
if asset.type == ASSET_TYPE_VIDEO:
asset_video_url = self._get_asset_video_url(asset.id)
if asset_video_url:
asset_detail[ATTR_ASSET_PLAYBACK_URL] = asset_video_url
added_assets_detail.append(asset_detail) added_assets_detail.append(asset_detail)
event_data = { event_data = {

View File

@@ -40,7 +40,7 @@ from .const import (
CONF_HUB_NAME, CONF_HUB_NAME,
CONF_TELEGRAM_BOT_TOKEN, CONF_TELEGRAM_BOT_TOKEN,
DOMAIN, DOMAIN,
SERVICE_GET_RECENT_ASSETS, SERVICE_GET_ASSETS,
SERVICE_REFRESH, SERVICE_REFRESH,
SERVICE_SEND_TELEGRAM_NOTIFICATION, SERVICE_SEND_TELEGRAM_NOTIFICATION,
) )
@@ -48,6 +48,10 @@ from .coordinator import AlbumData, ImmichAlbumWatcherCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Telegram photo limits
TELEGRAM_MAX_PHOTO_SIZE = 10 * 1024 * 1024 # 10 MB - Telegram's max photo size
TELEGRAM_MAX_DIMENSION_SUM = 10000 # Maximum sum of width + height in pixels
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@@ -88,13 +92,18 @@ async def async_setup_entry(
) )
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_GET_RECENT_ASSETS, SERVICE_GET_ASSETS,
{ {
vol.Optional("count", default=10): vol.All( vol.Optional("count", default=10): vol.All(
vol.Coerce(int), vol.Range(min=1, max=100) vol.Coerce(int), vol.Range(min=1, max=100)
), ),
vol.Optional("filter", default="none"): vol.In(["none", "favorite", "rating"]),
vol.Optional("filter_min_rating", default=1): vol.All(
vol.Coerce(int), vol.Range(min=1, max=5)
),
vol.Optional("order", default="descending"): vol.In(["ascending", "descending", "random"]),
}, },
"async_get_recent_assets", "async_get_assets",
supports_response=SupportsResponse.ONLY, supports_response=SupportsResponse.ONLY,
) )
@@ -171,9 +180,20 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
"""Refresh data for this album.""" """Refresh data for this album."""
await self.coordinator.async_refresh_now() await self.coordinator.async_refresh_now()
async def async_get_recent_assets(self, count: int = 10) -> ServiceResponse: async def async_get_assets(
"""Get recent assets for this album.""" self,
assets = await self.coordinator.async_get_recent_assets(count) count: int = 10,
filter: str = "none",
filter_min_rating: int = 1,
order: str = "descending",
) -> ServiceResponse:
"""Get assets for this album with optional filtering and ordering."""
assets = await self.coordinator.async_get_assets(
count=count,
filter=filter,
filter_min_rating=filter_min_rating,
order=order,
)
return {"assets": assets} return {"assets": assets}
async def async_send_telegram_notification( async def async_send_telegram_notification(
@@ -342,6 +362,60 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
_LOGGER.error("Telegram message send failed: %s", err) _LOGGER.error("Telegram message send failed: %s", err)
return {"success": False, "error": str(err)} return {"success": False, "error": str(err)}
def _log_telegram_error(
self,
error_code: int | None,
description: str,
data: bytes | None = None,
media_type: str = "photo",
) -> None:
"""Log detailed Telegram API error with diagnostics.
Args:
error_code: Telegram error code
description: Error description from Telegram
data: Media data bytes (optional, for size diagnostics)
media_type: Type of media (photo/video)
"""
error_msg = f"Telegram API error ({error_code}): {description}"
# Add diagnostic information based on error type
if data:
error_msg += f" | Media size: {len(data)} bytes ({len(data) / (1024 * 1024):.2f} MB)"
# Check dimensions for photos
if media_type == "photo":
try:
from PIL import Image
import io
img = Image.open(io.BytesIO(data))
width, height = img.size
dimension_sum = width + height
error_msg += f" | Dimensions: {width}x{height} (sum={dimension_sum})"
# Highlight limit violations
if len(data) > TELEGRAM_MAX_PHOTO_SIZE:
error_msg += f" | EXCEEDS size limit ({TELEGRAM_MAX_PHOTO_SIZE / (1024 * 1024):.0f} MB)"
if dimension_sum > TELEGRAM_MAX_DIMENSION_SUM:
error_msg += f" | EXCEEDS dimension limit ({TELEGRAM_MAX_DIMENSION_SUM})"
except Exception:
pass
# Provide suggestions based on error description
suggestions = []
if "dimension" in description.lower() or "PHOTO_INVALID_DIMENSIONS" in description:
suggestions.append("Photo dimensions too large - consider setting send_large_photos_as_documents=true")
elif "too large" in description.lower() or error_code == 413:
suggestions.append("File size too large - consider setting send_large_photos_as_documents=true or max_asset_data_size to skip large files")
elif "entity too large" in description.lower():
suggestions.append("Request entity too large - reduce max_group_size or set max_asset_data_size")
if suggestions:
error_msg += f" | Suggestions: {'; '.join(suggestions)}"
_LOGGER.error(error_msg)
def _check_telegram_photo_limits( def _check_telegram_photo_limits(
self, self,
data: bytes, data: bytes,
@@ -359,9 +433,6 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
- width: Image width in pixels (None if PIL not available) - width: Image width in pixels (None if PIL not available)
- height: Image height in pixels (None if PIL not available) - height: Image height in pixels (None if PIL not available)
""" """
TELEGRAM_MAX_PHOTO_SIZE = 10 * 1024 * 1024 # 10 MB
TELEGRAM_MAX_DIMENSION_SUM = 10000
# Check file size # Check file size
if len(data) > TELEGRAM_MAX_PHOTO_SIZE: if len(data) > TELEGRAM_MAX_PHOTO_SIZE:
return True, f"size {len(data)} bytes exceeds {TELEGRAM_MAX_PHOTO_SIZE} bytes limit", None, None return True, f"size {len(data)} bytes exceeds {TELEGRAM_MAX_PHOTO_SIZE} bytes limit", None, None
@@ -388,73 +459,6 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
_LOGGER.debug("Failed to check photo dimensions: %s", e) _LOGGER.debug("Failed to check photo dimensions: %s", e)
return False, None, None, None return False, None, None, None
def _downsize_photo(
self,
data: bytes,
max_dimension_sum: int = 10000,
max_file_size: int = 9 * 1024 * 1024, # 9 MB to be safe
) -> tuple[bytes | None, str | None]:
"""Downsize photo to fit within Telegram limits.
Args:
data: Original photo bytes
max_dimension_sum: Maximum sum of width + height
max_file_size: Maximum file size in bytes
Returns:
Tuple of (downsized_data, error)
- downsized_data: Downsized photo bytes (None if failed)
- error: Error message (None if successful)
"""
try:
from PIL import Image
import io
img = Image.open(io.BytesIO(data))
width, height = img.size
dimension_sum = width + height
# Calculate scale factor based on dimensions
if dimension_sum > max_dimension_sum:
scale = max_dimension_sum / dimension_sum
new_width = int(width * scale)
new_height = int(height * scale)
_LOGGER.info(
"Downsizing photo from %dx%d to %dx%d to fit Telegram limits",
width, height, new_width, new_height
)
# Resize with high-quality Lanczos resampling
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
# Save with progressive quality reduction until size fits
output = io.BytesIO()
quality = 95
while quality >= 50:
output.seek(0)
output.truncate()
img.save(output, format='JPEG', quality=quality, optimize=True)
output_size = output.tell()
if output_size <= max_file_size:
_LOGGER.debug(
"Photo downsized successfully: %d bytes (quality=%d)",
output_size, quality
)
return output.getvalue(), None
quality -= 5
# If we can't get it small enough, return error
return None, f"Unable to downsize photo below {max_file_size} bytes (final size: {output_size} bytes at quality {quality + 5})"
except ImportError:
return None, "PIL not available, cannot downsize photo"
except Exception as e:
_LOGGER.error("Failed to downsize photo: %s", e)
return None, f"Failed to downsize photo: {e}"
async def _send_telegram_photo( async def _send_telegram_photo(
self, self,
@@ -510,16 +514,13 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
caption, reply_to_message_id, parse_mode caption, reply_to_message_id, parse_mode
) )
else: else:
# Try to downsize the photo # Skip oversized photo
_LOGGER.info("Photo %s, attempting to downsize", reason) _LOGGER.warning("Photo %s, skipping (set send_large_photos_as_documents=true to send as document)", reason)
downsized_data, error = self._downsize_photo(data) return {
if downsized_data: "success": False,
data = downsized_data "error": f"Photo {reason}",
else: "skipped": True,
return { }
"success": False,
"error": f"Photo exceeds Telegram limits and downsize failed: {error}",
}
# Build multipart form # Build multipart form
form = FormData() form = FormData()
@@ -546,7 +547,13 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
"message_id": result.get("result", {}).get("message_id"), "message_id": result.get("result", {}).get("message_id"),
} }
else: else:
_LOGGER.error("Telegram API error: %s", result) # Log detailed error with diagnostics
self._log_telegram_error(
error_code=result.get("error_code"),
description=result.get("description", "Unknown Telegram error"),
data=data,
media_type="photo",
)
return { return {
"success": False, "success": False,
"error": result.get("description", "Unknown Telegram error"), "error": result.get("description", "Unknown Telegram error"),
@@ -623,7 +630,13 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
"message_id": result.get("result", {}).get("message_id"), "message_id": result.get("result", {}).get("message_id"),
} }
else: else:
_LOGGER.error("Telegram API error: %s", result) # Log detailed error with diagnostics
self._log_telegram_error(
error_code=result.get("error_code"),
description=result.get("description", "Unknown Telegram error"),
data=data,
media_type="video",
)
return { return {
"success": False, "success": False,
"error": result.get("description", "Unknown Telegram error"), "error": result.get("description", "Unknown Telegram error"),
@@ -674,7 +687,13 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
"message_id": result.get("result", {}).get("message_id"), "message_id": result.get("result", {}).get("message_id"),
} }
else: else:
_LOGGER.error("Telegram API error: %s", result) # Log detailed error with diagnostics
self._log_telegram_error(
error_code=result.get("error_code"),
description=result.get("description", "Unknown Telegram error"),
data=data,
media_type="document",
)
return { return {
"success": False, "success": False,
"error": result.get("description", "Unknown Telegram error"), "error": result.get("description", "Unknown Telegram error"),
@@ -756,7 +775,7 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
_LOGGER.debug("Sending chunk %d/%d as media group (%d items)", chunk_idx + 1, len(chunks), len(chunk)) _LOGGER.debug("Sending chunk %d/%d as media group (%d items)", chunk_idx + 1, len(chunks), len(chunk))
# Download all media files for this chunk # Download all media files for this chunk
media_files: list[tuple[str, bytes, str]] = [] media_files: list[tuple[str, bytes, str]] = [] # (type, data, filename)
oversized_photos: list[tuple[bytes, str | None]] = [] # For send_large_photos_as_documents=true oversized_photos: list[tuple[bytes, str | None]] = [] # For send_large_photos_as_documents=true
skipped_count = 0 skipped_count = 0
@@ -808,15 +827,10 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
_LOGGER.info("Photo %d %s, will send as document", i, reason) _LOGGER.info("Photo %d %s, will send as document", i, reason)
continue continue
else: else:
# Try to downsize the photo # Skip oversized photo
_LOGGER.info("Photo %d %s, attempting to downsize", i, reason) _LOGGER.warning("Photo %d %s, skipping (set send_large_photos_as_documents=true to send as document)", i, reason)
downsized_data, error = self._downsize_photo(data) skipped_count += 1
if downsized_data: continue
data = downsized_data
else:
_LOGGER.error("Failed to downsize photo %d: %s, skipping", i, error)
skipped_count += 1
continue
ext = "jpg" if media_type == "photo" else "mp4" ext = "jpg" if media_type == "photo" else "mp4"
filename = f"media_{chunk_idx * max_group_size + i}.{ext}" filename = f"media_{chunk_idx * max_group_size + i}.{ext}"
@@ -877,7 +891,26 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
] ]
all_message_ids.extend(chunk_message_ids) all_message_ids.extend(chunk_message_ids)
else: else:
_LOGGER.error("Telegram API error for chunk %d: %s", chunk_idx + 1, result) # Log detailed error for media group with total size info
total_size = sum(len(d) for _, d, _ in media_files)
_LOGGER.error(
"Telegram API error for chunk %d/%d: %s | Media count: %d | Total size: %d bytes (%.2f MB)",
chunk_idx + 1, len(chunks),
result.get("description", "Unknown Telegram error"),
len(media_files),
total_size,
total_size / (1024 * 1024)
)
# Log detailed diagnostics for the first photo in the group
for media_type, data, _ in media_files:
if media_type == "photo":
self._log_telegram_error(
error_code=result.get("error_code"),
description=result.get("description", "Unknown Telegram error"),
data=data,
media_type="photo",
)
break # Only log details for first photo
return { return {
"success": False, "success": False,
"error": result.get("description", "Unknown Telegram error"), "error": result.get("description", "Unknown Telegram error"),

View File

@@ -6,9 +6,9 @@ refresh:
integration: immich_album_watcher integration: immich_album_watcher
domain: sensor domain: sensor
get_recent_assets: get_assets:
name: Get Recent Assets name: Get Assets
description: Get the most recent assets from the targeted album. description: Get assets from the targeted album with optional filtering and ordering.
target: target:
entity: entity:
integration: immich_album_watcher integration: immich_album_watcher
@@ -16,7 +16,7 @@ get_recent_assets:
fields: fields:
count: count:
name: Count name: Count
description: Number of recent assets to return (1-100). description: Maximum number of assets to return (1-100).
required: false required: false
default: 10 default: 10
selector: selector:
@@ -24,6 +24,44 @@ get_recent_assets:
min: 1 min: 1
max: 100 max: 100
mode: slider mode: slider
filter:
name: Filter
description: Filter assets by type (none, favorite, or rating-based).
required: false
default: "none"
selector:
select:
options:
- label: "None (no filtering)"
value: "none"
- label: "Favorites only"
value: "favorite"
- label: "By minimum rating"
value: "rating"
filter_min_rating:
name: Minimum Rating
description: Minimum rating for assets (1-5). Only used when filter is set to 'rating'.
required: false
default: 1
selector:
number:
min: 1
max: 5
mode: slider
order:
name: Order
description: Sort order for assets by creation date.
required: false
default: "descending"
selector:
select:
options:
- label: "Ascending (oldest first)"
value: "ascending"
- label: "Descending (newest first)"
value: "descending"
- label: "Random"
value: "random"
send_telegram_notification: send_telegram_notification:
name: Send Telegram Notification name: Send Telegram Notification

View File

@@ -133,13 +133,25 @@
"name": "Refresh", "name": "Refresh",
"description": "Force an immediate refresh of album data from Immich." "description": "Force an immediate refresh of album data from Immich."
}, },
"get_recent_assets": { "get_assets": {
"name": "Get Recent Assets", "name": "Get Assets",
"description": "Get the most recent assets from the targeted album.", "description": "Get assets from the targeted album with optional filtering and ordering.",
"fields": { "fields": {
"count": { "count": {
"name": "Count", "name": "Count",
"description": "Number of recent assets to return (1-100)." "description": "Maximum number of assets to return (1-100)."
},
"filter": {
"name": "Filter",
"description": "Filter assets by type (none, favorite, or rating-based)."
},
"filter_min_rating": {
"name": "Minimum Rating",
"description": "Minimum rating for assets (1-5). Only used when filter is set to 'rating'."
},
"order": {
"name": "Order",
"description": "Sort order for assets by creation date."
} }
} }
}, },

View File

@@ -133,13 +133,25 @@
"name": "Обновить", "name": "Обновить",
"description": "Принудительно обновить данные альбома из Immich." "description": "Принудительно обновить данные альбома из Immich."
}, },
"get_recent_assets": { "get_assets": {
"name": "Получить последние файлы", "name": "Получить файлы",
"description": "Получить последние файлы из выбранного альбома.", "description": "Получить файлы из выбранного альбома с возможностью фильтрации и сортировки.",
"fields": { "fields": {
"count": { "count": {
"name": "Количество", "name": "Количество",
"description": "Количество возвращаемых файлов (1-100)." "description": "Максимальное количество возвращаемых файлов (1-100)."
},
"filter": {
"name": "Фильтр",
"description": "Фильтровать файлы по типу (none - без фильтра, favorite - только избранные, rating - по рейтингу)."
},
"filter_min_rating": {
"name": "Минимальный рейтинг",
"description": "Минимальный рейтинг для файлов (1-5). Используется только при filter='rating'."
},
"order": {
"name": "Порядок",
"description": "Порядок сортировки файлов по дате создания."
} }
} }
}, },