Add hub support

This commit is contained in:
2026-01-30 02:57:45 +03:00
parent 60573374a4
commit 82f293d0df
11 changed files with 50 additions and 16 deletions

View File

@@ -12,6 +12,7 @@ from .const import (
CONF_ALBUM_ID, CONF_ALBUM_ID,
CONF_ALBUM_NAME, CONF_ALBUM_NAME,
CONF_API_KEY, CONF_API_KEY,
CONF_HUB_NAME,
CONF_IMMICH_URL, CONF_IMMICH_URL,
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
@@ -27,6 +28,7 @@ _LOGGER = logging.getLogger(__name__)
class ImmichHubData: class ImmichHubData:
"""Data for the Immich hub.""" """Data for the Immich hub."""
name: str
url: str url: str
api_key: str api_key: str
scan_interval: int scan_interval: int
@@ -48,12 +50,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ImmichConfigEntry) -> bo
"""Set up Immich Album Watcher hub from a config entry.""" """Set up Immich Album Watcher hub from a config entry."""
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hub_name = entry.data.get(CONF_HUB_NAME, "Immich")
url = entry.data[CONF_IMMICH_URL] url = entry.data[CONF_IMMICH_URL]
api_key = entry.data[CONF_API_KEY] api_key = entry.data[CONF_API_KEY]
scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
# Store hub data # Store hub data
entry.runtime_data = ImmichHubData( entry.runtime_data = ImmichHubData(
name=hub_name,
url=url, url=url,
api_key=api_key, api_key=api_key,
scan_interval=scan_interval, scan_interval=scan_interval,
@@ -104,6 +108,7 @@ async def _async_setup_subentry_coordinator(
album_id=album_id, album_id=album_id,
album_name=album_name, album_name=album_name,
scan_interval=hub_data.scan_interval, scan_interval=hub_data.scan_interval,
hub_name=hub_data.name,
) )
# Fetch initial data # Fetch initial data

View File

@@ -15,12 +15,14 @@ from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify
from .const import ( from .const import (
ATTR_ALBUM_ID, ATTR_ALBUM_ID,
ATTR_ALBUM_NAME, ATTR_ALBUM_NAME,
CONF_ALBUM_ID, CONF_ALBUM_ID,
CONF_ALBUM_NAME, CONF_ALBUM_NAME,
CONF_HUB_NAME,
DOMAIN, DOMAIN,
NEW_ASSETS_RESET_DELAY, NEW_ASSETS_RESET_DELAY,
) )
@@ -71,7 +73,9 @@ class ImmichAlbumNewAssetsSensor(
self._subentry = subentry self._subentry = subentry
self._album_id = subentry.data[CONF_ALBUM_ID] self._album_id = subentry.data[CONF_ALBUM_ID]
self._album_name = subentry.data.get(CONF_ALBUM_NAME, "Unknown Album") self._album_name = subentry.data.get(CONF_ALBUM_NAME, "Unknown Album")
self._attr_unique_id = f"{subentry.subentry_id}_new_assets" self._hub_name = entry.data.get(CONF_HUB_NAME, "Immich")
unique_id_prefix = slugify(f"{self._hub_name}_album_{self._album_name}")
self._attr_unique_id = f"{unique_id_prefix}_new_assets"
@property @property
def _album_data(self) -> AlbumData | None: def _album_data(self) -> AlbumData | None:

View File

@@ -15,8 +15,9 @@ from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify
from .const import CONF_ALBUM_ID, CONF_ALBUM_NAME, DOMAIN from .const import CONF_ALBUM_ID, CONF_ALBUM_NAME, CONF_HUB_NAME, DOMAIN
from .coordinator import AlbumData, ImmichAlbumWatcherCoordinator from .coordinator import AlbumData, ImmichAlbumWatcherCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -66,7 +67,9 @@ class ImmichAlbumThumbnailCamera(
self._subentry = subentry self._subentry = subentry
self._album_id = subentry.data[CONF_ALBUM_ID] self._album_id = subentry.data[CONF_ALBUM_ID]
self._album_name = subentry.data.get(CONF_ALBUM_NAME, "Unknown Album") self._album_name = subentry.data.get(CONF_ALBUM_NAME, "Unknown Album")
self._attr_unique_id = f"{subentry.subentry_id}_thumbnail" self._hub_name = entry.data.get(CONF_HUB_NAME, "Immich")
unique_id_prefix = slugify(f"{self._hub_name}_album_{self._album_name}")
self._attr_unique_id = f"{unique_id_prefix}_thumbnail"
self._cached_image: bytes | None = None self._cached_image: bytes | None = None
self._last_thumbnail_id: str | None = None self._last_thumbnail_id: str | None = None

View File

@@ -23,6 +23,7 @@ from .const import (
CONF_ALBUM_ID, CONF_ALBUM_ID,
CONF_ALBUM_NAME, CONF_ALBUM_NAME,
CONF_API_KEY, CONF_API_KEY,
CONF_HUB_NAME,
CONF_IMMICH_URL, CONF_IMMICH_URL,
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
DEFAULT_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL,
@@ -92,6 +93,7 @@ class ImmichAlbumWatcherConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
hub_name = user_input[CONF_HUB_NAME].strip()
self._url = user_input[CONF_IMMICH_URL].rstrip("/") self._url = user_input[CONF_IMMICH_URL].rstrip("/")
self._api_key = user_input[CONF_API_KEY] self._api_key = user_input[CONF_API_KEY]
@@ -105,8 +107,9 @@ class ImmichAlbumWatcherConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry( return self.async_create_entry(
title="Immich Album Watcher", title=hub_name,
data={ data={
CONF_HUB_NAME: hub_name,
CONF_IMMICH_URL: self._url, CONF_IMMICH_URL: self._url,
CONF_API_KEY: self._api_key, CONF_API_KEY: self._api_key,
}, },
@@ -129,6 +132,7 @@ class ImmichAlbumWatcherConfigFlow(ConfigFlow, domain=DOMAIN):
step_id="user", step_id="user",
data_schema=vol.Schema( data_schema=vol.Schema(
{ {
vol.Required(CONF_HUB_NAME, default="Immich"): str,
vol.Required(CONF_IMMICH_URL): str, vol.Required(CONF_IMMICH_URL): str,
vol.Required(CONF_API_KEY): str, vol.Required(CONF_API_KEY): str,
} }

View File

@@ -6,6 +6,7 @@ from typing import Final
DOMAIN: Final = "immich_album_watcher" DOMAIN: Final = "immich_album_watcher"
# Configuration keys # Configuration keys
CONF_HUB_NAME: Final = "hub_name"
CONF_IMMICH_URL: Final = "immich_url" CONF_IMMICH_URL: Final = "immich_url"
CONF_API_KEY: Final = "api_key" CONF_API_KEY: Final = "api_key"
CONF_ALBUMS: Final = "albums" CONF_ALBUMS: Final = "albums"
@@ -26,6 +27,7 @@ EVENT_ASSETS_ADDED: Final = f"{DOMAIN}_assets_added"
EVENT_ASSETS_REMOVED: Final = f"{DOMAIN}_assets_removed" EVENT_ASSETS_REMOVED: Final = f"{DOMAIN}_assets_removed"
# Attributes # Attributes
ATTR_HUB_NAME: Final = "hub_name"
ATTR_ALBUM_ID: Final = "album_id" ATTR_ALBUM_ID: Final = "album_id"
ATTR_ALBUM_NAME: Final = "album_name" ATTR_ALBUM_NAME: Final = "album_name"
ATTR_ALBUM_URL: Final = "album_url" ATTR_ALBUM_URL: Final = "album_url"

View File

@@ -29,6 +29,7 @@ from .const import (
ATTR_ASSET_TYPE, ATTR_ASSET_TYPE,
ATTR_ASSET_URL, ATTR_ASSET_URL,
ATTR_CHANGE_TYPE, ATTR_CHANGE_TYPE,
ATTR_HUB_NAME,
ATTR_PEOPLE, ATTR_PEOPLE,
ATTR_REMOVED_ASSETS, ATTR_REMOVED_ASSETS,
ATTR_REMOVED_COUNT, ATTR_REMOVED_COUNT,
@@ -217,6 +218,7 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
album_id: str, album_id: str,
album_name: str, album_name: str,
scan_interval: int, scan_interval: int,
hub_name: str = "Immich",
) -> None: ) -> None:
"""Initialize the coordinator.""" """Initialize the coordinator."""
super().__init__( super().__init__(
@@ -229,6 +231,7 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
self._api_key = api_key self._api_key = api_key
self._album_id = album_id self._album_id = album_id
self._album_name = album_name self._album_name = album_name
self._hub_name = hub_name
self._previous_state: AlbumData | None = None self._previous_state: AlbumData | None = None
self._session: aiohttp.ClientSession | None = None self._session: aiohttp.ClientSession | None = None
self._people_cache: dict[str, str] = {} # person_id -> name self._people_cache: dict[str, str] = {} # person_id -> name
@@ -542,6 +545,7 @@ class ImmichAlbumWatcherCoordinator(DataUpdateCoordinator[AlbumData | None]):
added_assets_detail.append(asset_detail) added_assets_detail.append(asset_detail)
event_data = { event_data = {
ATTR_HUB_NAME: self._hub_name,
ATTR_ALBUM_ID: change.album_id, ATTR_ALBUM_ID: change.album_id,
ATTR_ALBUM_NAME: change.album_name, ATTR_ALBUM_NAME: change.album_name,
ATTR_CHANGE_TYPE: change.change_type, ATTR_CHANGE_TYPE: change.change_type,

View File

@@ -8,6 +8,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"issue_tracker": "https://github.com/your-repo/immich-album-watcher/issues", "issue_tracker": "https://github.com/your-repo/immich-album-watcher/issues",
"requirements": [], "requirements": [],
"single_config_entry": true, "version": "1.2.0"
"version": "1.1.0"
} }

View File

@@ -20,6 +20,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify
from .const import ( from .const import (
ATTR_ALBUM_ID, ATTR_ALBUM_ID,
@@ -36,6 +37,7 @@ from .const import (
ATTR_VIDEO_COUNT, ATTR_VIDEO_COUNT,
CONF_ALBUM_ID, CONF_ALBUM_ID,
CONF_ALBUM_NAME, CONF_ALBUM_NAME,
CONF_HUB_NAME,
DOMAIN, DOMAIN,
SERVICE_GET_RECENT_ASSETS, SERVICE_GET_RECENT_ASSETS,
SERVICE_REFRESH, SERVICE_REFRESH,
@@ -112,6 +114,9 @@ class ImmichAlbumBaseSensor(CoordinatorEntity[ImmichAlbumWatcherCoordinator], Se
self._subentry = subentry self._subentry = subentry
self._album_id = subentry.data[CONF_ALBUM_ID] self._album_id = subentry.data[CONF_ALBUM_ID]
self._album_name = subentry.data.get(CONF_ALBUM_NAME, "Unknown Album") self._album_name = subentry.data.get(CONF_ALBUM_NAME, "Unknown Album")
self._hub_name = entry.data.get(CONF_HUB_NAME, "Immich")
# Generate unique_id prefix: {hub_name}_album_{album_name}
self._unique_id_prefix = slugify(f"{self._hub_name}_album_{self._album_name}")
@property @property
def _album_data(self) -> AlbumData | None: def _album_data(self) -> AlbumData | None:
@@ -171,7 +176,7 @@ class ImmichAlbumAssetCountSensor(ImmichAlbumBaseSensor):
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator, entry, subentry) super().__init__(coordinator, entry, subentry)
self._attr_unique_id = f"{subentry.subentry_id}_asset_count" self._attr_unique_id = f"{self._unique_id_prefix}_asset_count"
@property @property
def native_value(self) -> int | None: def native_value(self) -> int | None:
@@ -222,7 +227,7 @@ class ImmichAlbumPhotoCountSensor(ImmichAlbumBaseSensor):
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator, entry, subentry) super().__init__(coordinator, entry, subentry)
self._attr_unique_id = f"{subentry.subentry_id}_photo_count" self._attr_unique_id = f"{self._unique_id_prefix}_photo_count"
@property @property
def native_value(self) -> int | None: def native_value(self) -> int | None:
@@ -247,7 +252,7 @@ class ImmichAlbumVideoCountSensor(ImmichAlbumBaseSensor):
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator, entry, subentry) super().__init__(coordinator, entry, subentry)
self._attr_unique_id = f"{subentry.subentry_id}_video_count" self._attr_unique_id = f"{self._unique_id_prefix}_video_count"
@property @property
def native_value(self) -> int | None: def native_value(self) -> int | None:
@@ -272,7 +277,7 @@ class ImmichAlbumLastUpdatedSensor(ImmichAlbumBaseSensor):
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator, entry, subentry) super().__init__(coordinator, entry, subentry)
self._attr_unique_id = f"{subentry.subentry_id}_last_updated" self._attr_unique_id = f"{self._unique_id_prefix}_last_updated"
@property @property
def native_value(self) -> datetime | None: def native_value(self) -> datetime | None:
@@ -302,7 +307,7 @@ class ImmichAlbumCreatedSensor(ImmichAlbumBaseSensor):
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator, entry, subentry) super().__init__(coordinator, entry, subentry)
self._attr_unique_id = f"{subentry.subentry_id}_created" self._attr_unique_id = f"{self._unique_id_prefix}_created"
@property @property
def native_value(self) -> datetime | None: def native_value(self) -> datetime | None:
@@ -332,7 +337,7 @@ class ImmichAlbumPeopleSensor(ImmichAlbumBaseSensor):
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator, entry, subentry) super().__init__(coordinator, entry, subentry)
self._attr_unique_id = f"{subentry.subentry_id}_people_count" self._attr_unique_id = f"{self._unique_id_prefix}_people_count"
@property @property
def native_value(self) -> int | None: def native_value(self) -> int | None:
@@ -366,7 +371,7 @@ class ImmichAlbumPublicUrlSensor(ImmichAlbumBaseSensor):
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator, entry, subentry) super().__init__(coordinator, entry, subentry)
self._attr_unique_id = f"{subentry.subentry_id}_public_url" self._attr_unique_id = f"{self._unique_id_prefix}_public_url"
@property @property
def native_value(self) -> str | None: def native_value(self) -> str | None:
@@ -411,7 +416,7 @@ class ImmichAlbumProtectedUrlSensor(ImmichAlbumBaseSensor):
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator, entry, subentry) super().__init__(coordinator, entry, subentry)
self._attr_unique_id = f"{subentry.subentry_id}_protected_url" self._attr_unique_id = f"{self._unique_id_prefix}_protected_url"
@property @property
def native_value(self) -> str | None: def native_value(self) -> str | None:
@@ -451,7 +456,7 @@ class ImmichAlbumProtectedPasswordSensor(ImmichAlbumBaseSensor):
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator, entry, subentry) super().__init__(coordinator, entry, subentry)
self._attr_unique_id = f"{subentry.subentry_id}_protected_password" self._attr_unique_id = f"{self._unique_id_prefix}_protected_password"
@property @property
def native_value(self) -> str | None: def native_value(self) -> str | None:

View File

@@ -11,12 +11,14 @@ from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify
from .const import ( from .const import (
ATTR_ALBUM_ID, ATTR_ALBUM_ID,
ATTR_ALBUM_PROTECTED_URL, ATTR_ALBUM_PROTECTED_URL,
CONF_ALBUM_ID, CONF_ALBUM_ID,
CONF_ALBUM_NAME, CONF_ALBUM_NAME,
CONF_HUB_NAME,
DOMAIN, DOMAIN,
) )
from .coordinator import AlbumData, ImmichAlbumWatcherCoordinator from .coordinator import AlbumData, ImmichAlbumWatcherCoordinator
@@ -68,7 +70,9 @@ class ImmichAlbumProtectedPasswordText(
self._subentry = subentry self._subentry = subentry
self._album_id = subentry.data[CONF_ALBUM_ID] self._album_id = subentry.data[CONF_ALBUM_ID]
self._album_name = subentry.data.get(CONF_ALBUM_NAME, "Unknown Album") self._album_name = subentry.data.get(CONF_ALBUM_NAME, "Unknown Album")
self._attr_unique_id = f"{subentry.subentry_id}_protected_password_edit" self._hub_name = entry.data.get(CONF_HUB_NAME, "Immich")
unique_id_prefix = slugify(f"{self._hub_name}_album_{self._album_name}")
self._attr_unique_id = f"{unique_id_prefix}_protected_password_edit"
@property @property
def _album_data(self) -> AlbumData | None: def _album_data(self) -> AlbumData | None:

View File

@@ -51,10 +51,12 @@
"title": "Connect to Immich", "title": "Connect to Immich",
"description": "Enter your Immich server details. You can get an API key from Immich → User Settings → API Keys.", "description": "Enter your Immich server details. You can get an API key from Immich → User Settings → API Keys.",
"data": { "data": {
"hub_name": "Hub Name",
"immich_url": "Immich URL", "immich_url": "Immich URL",
"api_key": "API Key" "api_key": "API Key"
}, },
"data_description": { "data_description": {
"hub_name": "A name for this Immich server (used in entity IDs)",
"immich_url": "The URL of your Immich server (e.g., http://192.168.1.100:2283)", "immich_url": "The URL of your Immich server (e.g., http://192.168.1.100:2283)",
"api_key": "Your Immich API key" "api_key": "Your Immich API key"
} }

View File

@@ -51,10 +51,12 @@
"title": "Подключение к Immich", "title": "Подключение к Immich",
"description": "Введите данные вашего сервера Immich. API-ключ можно получить в Immich → Настройки пользователя → API-ключи.", "description": "Введите данные вашего сервера Immich. API-ключ можно получить в Immich → Настройки пользователя → API-ключи.",
"data": { "data": {
"hub_name": "Название хаба",
"immich_url": "URL Immich", "immich_url": "URL Immich",
"api_key": "API-ключ" "api_key": "API-ключ"
}, },
"data_description": { "data_description": {
"hub_name": "Название для этого сервера Immich (используется в ID сущностей)",
"immich_url": "URL вашего сервера Immich (например, http://192.168.1.100:2283)", "immich_url": "URL вашего сервера Immich (например, http://192.168.1.100:2283)",
"api_key": "Ваш API-ключ Immich" "api_key": "Ваш API-ключ Immich"
} }