New apps Added
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
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={})
|
||||
Reference in New Issue
Block a user