124 files

This commit is contained in:
Home Assistant Version Control
2026-08-31 15:02:40 +00:00
parent 33de328a33
commit 9742f385a3
124 changed files with 13907 additions and 1376 deletions
@@ -78,9 +78,22 @@ from .const import (
CONF_NEXTCLOUD_FOLDER,
CONF_NEXTCLOUD_RECURSIVE,
CONF_NEXTCLOUD_IMAGE_SIZE,
CONF_NEXTCLOUD_SHARE_TOKEN,
CONF_NEXTCLOUD_AUTH_MODE,
NEXTCLOUD_AUTH_MODE_FOLDER,
NEXTCLOUD_AUTH_MODE_PUBLIC,
DEFAULT_NEXTCLOUD_AUTH_MODE,
DEFAULT_NEXTCLOUD_IMAGE_SIZE,
NEXTCLOUD_IMAGE_PREVIEW,
NEXTCLOUD_IMAGE_ORIGINAL,
CONF_ENTE_URL,
CONF_ENTE_ACCESS_TOKEN,
CONF_ENTE_COLLECTION_KEY,
CONF_ENTE_API_ORIGIN,
CONF_ENTE_IMAGE_SIZE,
DEFAULT_ENTE_IMAGE_SIZE,
ENTE_IMAGE_FULL,
ENTE_IMAGE_PREVIEW,
DEFAULT_REVERSE_GEOCODE,
PROVIDER_GOOGLE_SHARED,
PROVIDER_LOCAL_FOLDER,
@@ -90,6 +103,7 @@ from .const import (
PROVIDER_ICLOUD,
PROVIDER_SYNOLOGY,
PROVIDER_NEXTCLOUD,
PROVIDER_ENTE,
DEFAULT_RECURSIVE,
)
@@ -181,6 +195,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if config_entry.data.get(CONF_PROVIDER) in (
PROVIDER_LOCAL_FOLDER,
PROVIDER_NEXTCLOUD,
PROVIDER_ENTE,
):
return LocalFolderOptionsFlow()
return _NoOptionsFlow()
@@ -202,6 +217,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_synology()
if self._provider == PROVIDER_NEXTCLOUD:
return await self.async_step_nextcloud()
if self._provider == PROVIDER_ENTE:
return await self.async_step_ente()
return await self.async_step_google_shared()
schema = vol.Schema(
@@ -213,7 +230,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
PROVIDER_PHOTOPRISM: "PhotoPrism (direct API, full metadata)",
PROVIDER_ICLOUD: "iCloud Shared Album",
PROVIDER_SYNOLOGY: "Synology Photos (direct API, full metadata)",
PROVIDER_NEXTCLOUD: "Nextcloud (WebDAV folder, full metadata)",
PROVIDER_NEXTCLOUD: "Nextcloud (WebDAV folder or public album link)",
PROVIDER_ENTE: "Ente Photos (public album link)",
PROVIDER_MEDIA_SOURCE: "Media Source (any source, no metadata)",
})
}
@@ -976,6 +994,29 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_nextcloud(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Pick between an authenticated WebDAV folder and a public album link."""
if user_input is not None:
if user_input[CONF_NEXTCLOUD_AUTH_MODE] == NEXTCLOUD_AUTH_MODE_PUBLIC:
return await self.async_step_nextcloud_public()
return await self.async_step_nextcloud_folder()
schema = vol.Schema(
{
vol.Required(
CONF_NEXTCLOUD_AUTH_MODE, default=DEFAULT_NEXTCLOUD_AUTH_MODE
): vol.In(
{
NEXTCLOUD_AUTH_MODE_FOLDER: "Authenticated WebDAV folder",
NEXTCLOUD_AUTH_MODE_PUBLIC: "Public album link",
}
),
}
)
return self.async_show_form(step_id="nextcloud", data_schema=schema)
async def async_step_nextcloud_folder(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Collect and validate a Nextcloud WebDAV folder + app password."""
errors: dict[str, str] = {}
@@ -1010,6 +1051,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
title=name,
data={
CONF_PROVIDER: PROVIDER_NEXTCLOUD,
CONF_NEXTCLOUD_AUTH_MODE: NEXTCLOUD_AUTH_MODE_FOLDER,
CONF_NEXTCLOUD_URL: client.base_url,
CONF_NEXTCLOUD_USERNAME: username,
CONF_NEXTCLOUD_PASSWORD: password,
@@ -1043,7 +1085,138 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
}
)
return self.async_show_form(
step_id="nextcloud", data_schema=schema, errors=errors
step_id="nextcloud_folder", data_schema=schema, errors=errors
)
async def async_step_nextcloud_public(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Collect and validate a Nextcloud Photos public album link."""
errors: dict[str, str] = {}
if user_input is not None:
name = user_input[CONF_ALBUM_NAME].strip()
raw_url = user_input[CONF_NEXTCLOUD_URL].strip()
size = user_input.get(
CONF_NEXTCLOUD_IMAGE_SIZE, DEFAULT_NEXTCLOUD_IMAGE_SIZE
)
from . import nextcloud as nc_api
parsed = nc_api.parse_share_link(raw_url)
if parsed is None:
errors[CONF_NEXTCLOUD_URL] = "invalid_nextcloud_url"
else:
base_url, token = parsed
client = nc_api.NextcloudPublicClient(self.hass, base_url, token)
try:
await client.async_validate()
except Exception: # noqa: BLE001 - any failure means bad/expired link
errors["base"] = "nextcloud_public_cannot_connect"
else:
await self.async_set_unique_id(
f"{DOMAIN}:{PROVIDER_NEXTCLOUD}:{base_url}:{token}:{name}"
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=name,
data={
CONF_PROVIDER: PROVIDER_NEXTCLOUD,
CONF_NEXTCLOUD_AUTH_MODE: NEXTCLOUD_AUTH_MODE_PUBLIC,
CONF_NEXTCLOUD_URL: base_url,
CONF_NEXTCLOUD_SHARE_TOKEN: token,
CONF_NEXTCLOUD_IMAGE_SIZE: size,
CONF_ALBUM_NAME: name,
},
)
schema = vol.Schema(
{
vol.Required(CONF_ALBUM_NAME): str,
vol.Required(CONF_NEXTCLOUD_URL): str,
vol.Optional(
CONF_NEXTCLOUD_IMAGE_SIZE, default=DEFAULT_NEXTCLOUD_IMAGE_SIZE
): vol.In(
{
NEXTCLOUD_IMAGE_PREVIEW: "Preview (smoothest slideshow)",
NEXTCLOUD_IMAGE_ORIGINAL: "Original (full quality, slower)",
}
),
}
)
return self.async_show_form(
step_id="nextcloud_public", data_schema=schema, errors=errors
)
async def async_step_ente(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Collect and validate an Ente Photos public album link."""
errors: dict[str, str] = {}
if user_input is not None:
name = user_input[CONF_ALBUM_NAME].strip()
raw_url = user_input[CONF_ENTE_URL].strip()
api_origin = (user_input.get(CONF_ENTE_API_ORIGIN) or "").strip()
size = user_input.get(CONF_ENTE_IMAGE_SIZE, DEFAULT_ENTE_IMAGE_SIZE)
from . import ente as ente_api
share = ente_api.parse_share_link(raw_url)
if share is None:
errors[CONF_ENTE_URL] = "invalid_ente_url"
else:
client = ente_api.EnteClient(self.hass, share, api_origin)
try:
await client.async_validate()
except Exception as err: # noqa: BLE001 - bad, expired or private link
_LOGGER.warning(
"Ente validation failed for %s: %s",
share.album_origin,
_describe_error(err),
)
errors["base"] = "ente_cannot_connect"
else:
from base64 import b64encode
await self.async_set_unique_id(
f"{DOMAIN}:{PROVIDER_ENTE}:{share.access_token}:{name}"
)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=name,
data={
CONF_PROVIDER: PROVIDER_ENTE,
CONF_ENTE_URL: share.album_origin,
CONF_ENTE_ACCESS_TOKEN: share.access_token,
CONF_ENTE_COLLECTION_KEY: b64encode(
share.collection_key
).decode("ascii"),
CONF_ENTE_API_ORIGIN: ente_api.normalize_api_origin(
api_origin
),
CONF_ENTE_IMAGE_SIZE: size,
CONF_ALBUM_NAME: name,
},
)
schema = vol.Schema(
{
vol.Required(CONF_ALBUM_NAME): str,
vol.Required(CONF_ENTE_URL): str,
vol.Optional(
CONF_ENTE_IMAGE_SIZE, default=DEFAULT_ENTE_IMAGE_SIZE
): vol.In(
{
ENTE_IMAGE_FULL: "Full quality (original file)",
ENTE_IMAGE_PREVIEW: "Preview (faster, softer)",
}
),
vol.Optional(CONF_ENTE_API_ORIGIN): str,
}
)
return self.async_show_form(
step_id="ente", data_schema=schema, errors=errors
)