Add HAOS-Server sync for optional centralized management (Phase 5)
Some checks failed
Validate / Hassfest (push) Has been cancelled

Enable the HAOS integration to optionally connect to the standalone
Immich Watcher server for config sync and event reporting.

Server-side:
- New /api/sync/* endpoints: GET trackers, POST template render,
  POST event report
- API key auth via X-API-Key header (accepts JWT access tokens)

Integration-side:
- New sync.py: ServerSyncClient with graceful error handling
  (all methods return defaults on connection failure)
- Options flow: optional server_url and server_api_key fields
  with connection validation
- Coordinator: fire-and-forget event reporting to server when
  album changes are detected
- Translations: en.json and ru.json updated with new fields

The connection is fully additive -- the integration works identically
without a server URL configured. Server failures never break HA.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 14:10:29 +03:00
parent 2b487707ce
commit ab1c7ac0db
11 changed files with 441 additions and 20 deletions

View File

@@ -26,6 +26,8 @@ from .const import (
CONF_HUB_NAME,
CONF_IMMICH_URL,
CONF_SCAN_INTERVAL,
CONF_SERVER_API_KEY,
CONF_SERVER_URL,
CONF_TELEGRAM_BOT_TOKEN,
CONF_TELEGRAM_CACHE_TTL,
DEFAULT_SCAN_INTERVAL,
@@ -244,21 +246,40 @@ class ImmichAlbumWatcherOptionsFlow(OptionsFlow):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options."""
errors: dict[str, str] = {}
if user_input is not None:
return self.async_create_entry(
title="",
data={
CONF_SCAN_INTERVAL: user_input.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
),
CONF_TELEGRAM_BOT_TOKEN: user_input.get(
CONF_TELEGRAM_BOT_TOKEN, ""
),
CONF_TELEGRAM_CACHE_TTL: user_input.get(
CONF_TELEGRAM_CACHE_TTL, DEFAULT_TELEGRAM_CACHE_TTL
),
},
)
# Validate server connection if URL is provided
server_url = user_input.get(CONF_SERVER_URL, "").strip()
server_api_key = user_input.get(CONF_SERVER_API_KEY, "").strip()
if server_url and server_api_key:
try:
session = async_get_clientsession(self.hass)
async with session.get(
f"{server_url.rstrip('/')}/api/health"
) as response:
if response.status != 200:
errors["base"] = "server_connect_failed"
except Exception:
errors["base"] = "server_connect_failed"
if not errors:
return self.async_create_entry(
title="",
data={
CONF_SCAN_INTERVAL: user_input.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
),
CONF_TELEGRAM_BOT_TOKEN: user_input.get(
CONF_TELEGRAM_BOT_TOKEN, ""
),
CONF_TELEGRAM_CACHE_TTL: user_input.get(
CONF_TELEGRAM_CACHE_TTL, DEFAULT_TELEGRAM_CACHE_TTL
),
CONF_SERVER_URL: server_url,
CONF_SERVER_API_KEY: server_api_key,
},
)
return self.async_show_form(
step_id="init",
@@ -276,6 +297,12 @@ class ImmichAlbumWatcherOptionsFlow(OptionsFlow):
current_cache_ttl = self._config_entry.options.get(
CONF_TELEGRAM_CACHE_TTL, DEFAULT_TELEGRAM_CACHE_TTL
)
current_server_url = self._config_entry.options.get(
CONF_SERVER_URL, ""
)
current_server_api_key = self._config_entry.options.get(
CONF_SERVER_API_KEY, ""
)
return vol.Schema(
{
@@ -288,6 +315,13 @@ class ImmichAlbumWatcherOptionsFlow(OptionsFlow):
vol.Optional(
CONF_TELEGRAM_CACHE_TTL, default=current_cache_ttl
): vol.All(vol.Coerce(int), vol.Range(min=1, max=168)),
vol.Optional(
CONF_SERVER_URL, default=current_server_url,
description={"suggested_value": current_server_url},
): str,
vol.Optional(
CONF_SERVER_API_KEY, default=current_server_api_key,
): str,
}
)