from __future__ import annotations import re from typing import Any import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from .const import ( DOMAIN, CONF_PROVIDER, CONF_ALBUM_NAME, CONF_ALBUM_URL, CONF_LOCAL_PATH, CONF_RECURSIVE, CONF_REVERSE_GEOCODE, DEFAULT_REVERSE_GEOCODE, PROVIDER_GOOGLE_SHARED, PROVIDER_LOCAL_FOLDER, DEFAULT_RECURSIVE, ) def _normalize_local_path(hass, path: str) -> str: p = path.strip() if p.startswith("/local/"): p = "/config/www/" + p[len("/local/"):] elif p == "/local": p = "/config/www" elif p.startswith("local/"): p = "/config/www/" + p[len("local/"):] elif p.startswith("/media/local/"): p = "/media/" + p[len("/media/local/"):] elif p.startswith("media/local/"): p = "/media/" + p[len("media/local/"):] elif p.startswith("media/"): p = "/media/" + p[len("media/"):] elif p == "media": p = "/media" if not p.startswith("/"): p = hass.config.path(p) return p ALBUM_URL_RE = re.compile(r"^https?://photos\.app\.goo\.gl/[^/]+/?$") class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 def __init__(self) -> None: self._provider: str | None = None @staticmethod @callback def async_get_options_flow( config_entry: config_entries.ConfigEntry, ) -> config_entries.OptionsFlow: """Return the options flow handler. Only local-folder entries expose user-tunable options today (the reverse-geocode toggle); Google entries get a no-op handler so that the "Configure" button doesn't appear empty in the UI. Note: do NOT pass ``config_entry`` to the OptionsFlow constructor. Since Home Assistant 2024.12 the base class manages ``self.config_entry`` as a property and assigning to it in ``__init__`` raises (the symptom is a 500 when the user clicks Configure). """ if config_entry.data.get(CONF_PROVIDER) == PROVIDER_LOCAL_FOLDER: return LocalFolderOptionsFlow() return _NoOptionsFlow() async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult: if user_input is not None: self._provider = user_input[CONF_PROVIDER] if self._provider == PROVIDER_LOCAL_FOLDER: return await self.async_step_local_folder() return await self.async_step_google_shared() schema = vol.Schema( { vol.Required(CONF_PROVIDER, default=PROVIDER_GOOGLE_SHARED): vol.In({ PROVIDER_GOOGLE_SHARED: "Google Photos", PROVIDER_LOCAL_FOLDER: "Local Folder", }) } ) return self.async_show_form(step_id="user", data_schema=schema) async def async_step_google_shared(self, user_input: dict[str, Any] | None = None) -> FlowResult: errors: dict[str, str] = {} if user_input is not None: url = user_input[CONF_ALBUM_URL].strip() name = user_input[CONF_ALBUM_NAME].strip() if not ALBUM_URL_RE.match(url): errors[CONF_ALBUM_URL] = "invalid_album_url" else: await self.async_set_unique_id(f"{DOMAIN}:{PROVIDER_GOOGLE_SHARED}:{url}") self._abort_if_unique_id_configured() return self.async_create_entry( title=name, data={ CONF_PROVIDER: PROVIDER_GOOGLE_SHARED, CONF_ALBUM_URL: url, CONF_ALBUM_NAME: name, }, ) schema = vol.Schema( { vol.Required(CONF_ALBUM_NAME): str, vol.Required(CONF_ALBUM_URL): str, } ) return self.async_show_form(step_id="google_shared", data_schema=schema, errors=errors) async def async_step_local_folder(self, user_input: dict[str, Any] | None = None) -> FlowResult: errors: dict[str, str] = {} if user_input is not None: path = _normalize_local_path(self.hass, user_input[CONF_LOCAL_PATH]) name = user_input[CONF_ALBUM_NAME].strip() recursive = bool(user_input.get(CONF_RECURSIVE, DEFAULT_RECURSIVE)) if not path: errors[CONF_LOCAL_PATH] = "invalid_path" else: await self.async_set_unique_id(f"{DOMAIN}:{PROVIDER_LOCAL_FOLDER}:{path}") self._abort_if_unique_id_configured() return self.async_create_entry( title=name, data={ CONF_PROVIDER: PROVIDER_LOCAL_FOLDER, CONF_LOCAL_PATH: path, CONF_RECURSIVE: recursive, CONF_ALBUM_NAME: name, }, ) schema = vol.Schema( { vol.Required(CONF_ALBUM_NAME): str, vol.Required(CONF_LOCAL_PATH): str, vol.Optional(CONF_RECURSIVE, default=DEFAULT_RECURSIVE): bool, } ) return self.async_show_form(step_id="local_folder", data_schema=schema, errors=errors) class LocalFolderOptionsFlow(config_entries.OptionsFlow): """Options for local-folder entries. Currently exposes a single toggle: ``reverse_geocode``. Users with privacy concerns about handing EXIF GPS coordinates to an external OSM endpoint can turn this off; the GPS coordinates remain available as ``latitude``/``longitude`` attributes regardless. ``self.config_entry`` is provided by ``OptionsFlow`` as a managed property (HA 2024.12+); we deliberately do NOT define ``__init__`` or assign to it, since doing so raises in newer cores. """ async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> FlowResult: if user_input is not None: return self.async_create_entry(title="", data=user_input) current = self.config_entry.options.get( CONF_REVERSE_GEOCODE, DEFAULT_REVERSE_GEOCODE ) schema = vol.Schema( { vol.Required( CONF_REVERSE_GEOCODE, default=bool(current) ): bool, } ) return self.async_show_form(step_id="init", data_schema=schema) class _NoOptionsFlow(config_entries.OptionsFlow): """Fallback options flow for providers that expose nothing tunable.""" async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> FlowResult: return self.async_create_entry(title="", data={})