187 files
This commit is contained in:
1 parent
28fa34d78a
commit
1e1b12d52b
187 files changed
+40899
-32654
No files matched your search
@@ -99,6 +99,13 @@ from .const import (
|
||||
DEFAULT_ENTE_IMAGE_SIZE,
|
||||
ENTE_IMAGE_FULL,
|
||||
ENTE_IMAGE_PREVIEW,
|
||||
CONF_UGREEN_URL,
|
||||
CONF_UGREEN_USERNAME,
|
||||
CONF_UGREEN_PASSWORD,
|
||||
CONF_UGREEN_ALBUM_UUID,
|
||||
CONF_UGREEN_ALBUM_TYPE,
|
||||
CONF_UGREEN_ALBUM_NAME,
|
||||
CONF_UGREEN_VERIFY_SSL,
|
||||
DEFAULT_REVERSE_GEOCODE,
|
||||
PROVIDER_GOOGLE_SHARED,
|
||||
PROVIDER_LOCAL_FOLDER,
|
||||
@@ -109,6 +116,7 @@ from .const import (
|
||||
PROVIDER_SYNOLOGY,
|
||||
PROVIDER_NEXTCLOUD,
|
||||
PROVIDER_ENTE,
|
||||
PROVIDER_UGREEN,
|
||||
DEFAULT_RECURSIVE,
|
||||
)
|
||||
|
||||
@@ -374,6 +382,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self._syn_places: dict[str, str] = {}
|
||||
self._syn_tags: dict[str, str] = {}
|
||||
self._syn_subjects: dict[str, str] = {}
|
||||
# UGREEN flow state carried between steps.
|
||||
self._ugr_url: str | None = None
|
||||
self._ugr_username: str | None = None
|
||||
self._ugr_password: str | None = None
|
||||
self._ugr_verify_ssl: bool = True
|
||||
self._ugr_albums: list[dict[str, Any]] = []
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
@@ -382,8 +396,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
) -> config_entries.OptionsFlow:
|
||||
"""Return the options flow handler.
|
||||
|
||||
Local-folder, Nextcloud and Ente entries expose the reverse-geocode
|
||||
toggle; Immich entries reopen the albums/people/favorites picker.
|
||||
Local-folder, Nextcloud, Ente and UGREEN entries expose the
|
||||
reverse-geocode toggle; Immich entries reopen the
|
||||
albums/people/favorites picker.
|
||||
Other providers get a no-op handler so that the "Configure" button
|
||||
doesn't appear empty in the UI.
|
||||
|
||||
@@ -397,6 +412,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
PROVIDER_LOCAL_FOLDER,
|
||||
PROVIDER_NEXTCLOUD,
|
||||
PROVIDER_ENTE,
|
||||
PROVIDER_UGREEN,
|
||||
):
|
||||
return LocalFolderOptionsFlow()
|
||||
if config_entry.data.get(CONF_PROVIDER) == PROVIDER_IMMICH:
|
||||
@@ -424,6 +440,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_nextcloud()
|
||||
if self._provider == PROVIDER_ENTE:
|
||||
return await self.async_step_ente()
|
||||
if self._provider == PROVIDER_UGREEN:
|
||||
return await self.async_step_ugreen()
|
||||
return await self.async_step_google_shared()
|
||||
|
||||
schema = vol.Schema(
|
||||
@@ -437,6 +455,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
PROVIDER_SYNOLOGY: "Synology Photos (direct API, full metadata)",
|
||||
PROVIDER_NEXTCLOUD: "Nextcloud (WebDAV folder or public album link)",
|
||||
PROVIDER_ENTE: "Ente Photos (public album link)",
|
||||
PROVIDER_UGREEN: "UGREEN NAS (UGOS Photos, experimental)",
|
||||
PROVIDER_MEDIA_SOURCE: "Media Source (any source, no metadata)",
|
||||
})
|
||||
}
|
||||
@@ -1116,6 +1135,123 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
step_id="synology_select", data_schema=vol.Schema(fields), errors=errors
|
||||
)
|
||||
|
||||
async def async_step_ugreen(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Collect the UGREEN NAS URL + account and log in.
|
||||
|
||||
The album is picked from a live dropdown in the next step rather
|
||||
than typed in, so logging in here (and listing albums) doubles as
|
||||
credential validation.
|
||||
"""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
url = user_input[CONF_UGREEN_URL].strip()
|
||||
username = user_input[CONF_UGREEN_USERNAME].strip()
|
||||
password = user_input.get(CONF_UGREEN_PASSWORD) or ""
|
||||
verify_ssl = bool(user_input.get(CONF_UGREEN_VERIFY_SSL, True))
|
||||
|
||||
from . import ugreen as ugr_api
|
||||
|
||||
client = ugr_api.UGreenClient(
|
||||
self.hass, url, username, password, verify_ssl=verify_ssl
|
||||
)
|
||||
albums: list[dict[str, Any]] = []
|
||||
try:
|
||||
await client.async_login()
|
||||
albums = await client.async_list_albums()
|
||||
except ugr_api.UGreenAuthError as err:
|
||||
_LOGGER.warning(
|
||||
"UGREEN login failed for %s: %s", url, _describe_error(err)
|
||||
)
|
||||
errors["base"] = "ugreen_cannot_connect"
|
||||
except ugr_api.UGreenApiError as err:
|
||||
_LOGGER.warning(
|
||||
"UGREEN album listing failed for %s: %s", url, _describe_error(err)
|
||||
)
|
||||
errors["base"] = "ugreen_cannot_connect"
|
||||
|
||||
named_albums = [
|
||||
a for a in albums if isinstance(a, dict) and a.get("album_name")
|
||||
]
|
||||
if not errors and not named_albums:
|
||||
errors["base"] = "ugreen_no_albums"
|
||||
|
||||
if not errors:
|
||||
self._ugr_url = client.base_url
|
||||
self._ugr_username = username
|
||||
self._ugr_password = password
|
||||
self._ugr_verify_ssl = verify_ssl
|
||||
self._ugr_albums = named_albums
|
||||
return await self.async_step_ugreen_select()
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_UGREEN_URL): str,
|
||||
vol.Required(CONF_UGREEN_USERNAME): str,
|
||||
vol.Required(CONF_UGREEN_PASSWORD): selector.TextSelector(
|
||||
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
|
||||
),
|
||||
vol.Optional(CONF_UGREEN_VERIFY_SSL, default=True): bool,
|
||||
}
|
||||
)
|
||||
return self.async_show_form(step_id="ugreen", data_schema=schema, errors=errors)
|
||||
|
||||
async def async_step_ugreen_select(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Pick the UGOS Photos album from a live, searchable dropdown."""
|
||||
from . import ugreen as ugr_api
|
||||
|
||||
# Keyed by uuid since album names aren't guaranteed unique.
|
||||
albums_by_uuid = {a["album_uuid"]: a for a in self._ugr_albums}
|
||||
|
||||
if user_input is not None:
|
||||
album_uuid = user_input[CONF_UGREEN_ALBUM_UUID]
|
||||
album = albums_by_uuid[album_uuid]
|
||||
unique = (
|
||||
f"{DOMAIN}:{PROVIDER_UGREEN}:{self._ugr_url}:"
|
||||
f"{self._ugr_username}:{album_uuid}"
|
||||
)
|
||||
await self.async_set_unique_id(unique)
|
||||
self._abort_if_unique_id_configured()
|
||||
data = {
|
||||
CONF_PROVIDER: PROVIDER_UGREEN,
|
||||
CONF_UGREEN_URL: self._ugr_url,
|
||||
CONF_UGREEN_USERNAME: self._ugr_username,
|
||||
CONF_UGREEN_PASSWORD: self._ugr_password,
|
||||
CONF_UGREEN_VERIFY_SSL: self._ugr_verify_ssl,
|
||||
CONF_UGREEN_ALBUM_UUID: album_uuid,
|
||||
CONF_UGREEN_ALBUM_TYPE: album.get(
|
||||
"album_type", ugr_api.ALBUM_TYPE_REGULAR
|
||||
),
|
||||
CONF_UGREEN_ALBUM_NAME: album["album_name"],
|
||||
}
|
||||
return self.async_create_entry(title=album["album_name"], data=data)
|
||||
|
||||
# A searchable dropdown (rather than a plain vol.In) keeps this
|
||||
# usable for accounts with a large number of albums.
|
||||
options = [
|
||||
selector.SelectOptionDict(
|
||||
value=a["album_uuid"],
|
||||
label=f"{a['album_name']} ({ugr_api.describe_album_type(a.get('album_type'))})",
|
||||
)
|
||||
for a in self._ugr_albums
|
||||
]
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_UGREEN_ALBUM_UUID): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=options,
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
custom_value=False,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
return self.async_show_form(step_id="ugreen_select", data_schema=schema)
|
||||
|
||||
async def async_step_nextcloud(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
|
||||
Reference in new issue
Block a user