278 files
This commit is contained in:
1 parent
d7a0a08372
commit
1b4f5f68c6
278 files changed
+36892
-7150
No files matched your search
@@ -5,7 +5,16 @@ 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 homeassistant.helpers.selector import (
|
||||
NumberSelector,
|
||||
NumberSelectorConfig,
|
||||
NumberSelectorMode,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .api import SeaTemperatureAPI
|
||||
from .const import (
|
||||
@@ -14,16 +23,63 @@ from .const import (
|
||||
CONF_COUNTRY,
|
||||
CONF_PATH,
|
||||
CONF_PLACE,
|
||||
CONF_SCAN_INTERVAL_HOURS,
|
||||
DEFAULT_SCAN_INTERVAL_HOURS,
|
||||
DOMAIN,
|
||||
MAX_SCAN_INTERVAL_HOURS,
|
||||
MIN_SCAN_INTERVAL_HOURS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_CONTINENT_MINOR_WORDS = frozenset({"and", "of", "the"})
|
||||
|
||||
|
||||
def _searchable(options: list[str]) -> SelectSelector:
|
||||
"""Build a dropdown that filters as the user types.
|
||||
|
||||
vol.In renders a plain list, and the United States alone contributes 4,477
|
||||
places to it - unusable without a search field. Filtering happens in the
|
||||
frontend against the already fetched list, so it costs no extra request.
|
||||
"""
|
||||
return SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=options,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
sort=True,
|
||||
custom_value=False,
|
||||
)
|
||||
)
|
||||
|
||||
# The first path segment of every location, as published by
|
||||
# /api/map-locations.json. Slugs missing here fall back to a title-cased name,
|
||||
# so a stale entry is worse than none: it produces a second spelling of a
|
||||
# continent that no location actually resolves to.
|
||||
CONTINENT_NAMES = {
|
||||
"africa": "Africa",
|
||||
"antarctica": "Antarctica",
|
||||
"asia": "Asia",
|
||||
"australia-and-oceania": "Australia and Oceania",
|
||||
"central-america-and-the-caribbean": "Central America and the Caribbean",
|
||||
"europe": "Europe",
|
||||
"middle-east": "Middle East",
|
||||
"north-america": "North America",
|
||||
"south-america": "South America",
|
||||
}
|
||||
|
||||
|
||||
class SeaTemperatureConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Sea Temperature."""
|
||||
|
||||
VERSION = 2
|
||||
VERSION = 4
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
) -> SeaTemperatureOptionsFlow:
|
||||
"""Return the options flow handler."""
|
||||
return SeaTemperatureOptionsFlow()
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
@@ -37,29 +93,43 @@ class SeaTemperatureConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step (fetch map locations and select continent)."""
|
||||
api = SeaTemperatureAPI(self.hass)
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if self._locations_data is None:
|
||||
self._locations_data = await api._get_map_locations()
|
||||
if not self._locations_data:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
api = SeaTemperatureAPI(self.hass)
|
||||
locations = await api.get_map_locations()
|
||||
if not locations:
|
||||
# Re-show the form rather than aborting: "cannot_connect" only
|
||||
# exists under config.error in the translations, and an upstream
|
||||
# hiccup should be retryable without restarting the whole flow.
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
self._locations_data = locations
|
||||
|
||||
# Extract unique continents from path segments
|
||||
continents_set = set()
|
||||
for loc in self._locations_data.values():
|
||||
path = loc.get("path", "")
|
||||
parts = [p for p in path.split("/") if p]
|
||||
if parts:
|
||||
continents_set.add(self._get_continent_name(parts[0]))
|
||||
self._continents = sorted(continents_set)
|
||||
# Extract unique continents from path segments
|
||||
continents_set = set()
|
||||
for loc in self._locations_data.values():
|
||||
path = loc.get("path", "")
|
||||
parts = [p for p in path.split("/") if p]
|
||||
if parts:
|
||||
continents_set.add(self._get_continent_name(parts[0]))
|
||||
self._continents = sorted(continents_set)
|
||||
|
||||
if user_input is not None:
|
||||
# The retry form carries no continent field, so its submission has to fall
|
||||
# through to another fetch instead of being read as a selection.
|
||||
if user_input is not None and CONF_CONTINENT in user_input:
|
||||
self._data[CONF_CONTINENT] = user_input[CONF_CONTINENT]
|
||||
return await self.async_step_country()
|
||||
|
||||
if errors:
|
||||
# An empty schema still renders a submit button, which is the retry.
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=vol.Schema({}), errors=errors
|
||||
)
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_CONTINENT): vol.In(self._continents),
|
||||
vol.Required(CONF_CONTINENT): _searchable(self._continents),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -93,7 +163,7 @@ class SeaTemperatureConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_COUNTRY): vol.In(self._countries),
|
||||
vol.Required(CONF_COUNTRY): _searchable(self._countries),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -145,7 +215,7 @@ class SeaTemperatureConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLACE): vol.In(
|
||||
vol.Required(CONF_PLACE): _searchable(
|
||||
sorted(self._places.keys()) if self._places else []
|
||||
),
|
||||
}
|
||||
@@ -159,15 +229,67 @@ class SeaTemperatureConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
def _get_continent_name(self, slug: str) -> str:
|
||||
"""Map continent slug to friendly name."""
|
||||
continent_map = {
|
||||
"africa": "Africa",
|
||||
"asia": "Asia",
|
||||
"caribbean-sea": "Caribbean Sea",
|
||||
"central-america": "Central America",
|
||||
"europe": "Europe",
|
||||
"middle-east": "Middle East",
|
||||
"north-america": "North America",
|
||||
"oceania": "Oceania",
|
||||
"south-america": "South America",
|
||||
}
|
||||
return continent_map.get(slug, slug.replace("-", " ").title())
|
||||
if slug in CONTINENT_NAMES:
|
||||
return CONTINENT_NAMES[slug]
|
||||
|
||||
# A slug the site adds later still has to read as a name rather than as
|
||||
# "Central America And The Caribbean", so keep the joining words small.
|
||||
words = slug.split("-")
|
||||
return " ".join(
|
||||
word if index and word in _CONTINENT_MINOR_WORDS else word.title()
|
||||
for index, word in enumerate(words)
|
||||
)
|
||||
|
||||
|
||||
class SeaTemperatureOptionsFlow(config_entries.OptionsFlow):
|
||||
"""Let an existing entry be re-tuned without being deleted.
|
||||
|
||||
Only the poll interval is offered. Repointing an entry at a different beach
|
||||
is deliberately *not* an option: the entry's unique_id is the location path,
|
||||
and keeping the entity while swapping the place would splice two locations'
|
||||
readings into one long-term statistic - the history this flow exists to
|
||||
protect. A different place is a different entry.
|
||||
|
||||
The reload that makes a new interval take effect comes from the update
|
||||
listener ``async_setup_entry`` registers, not from a base class:
|
||||
``OptionsFlowWithReload`` only exists from core 2025.8 on, and buying that
|
||||
convenience with a narrower supported range is a bad trade. Note the two are
|
||||
mutually exclusive - core rejects an entry that has both.
|
||||
"""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the options step."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
data={
|
||||
CONF_SCAN_INTERVAL_HOURS: int(
|
||||
user_input[CONF_SCAN_INTERVAL_HOURS]
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
current = self.config_entry.options.get(
|
||||
CONF_SCAN_INTERVAL_HOURS, DEFAULT_SCAN_INTERVAL_HOURS
|
||||
)
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_SCAN_INTERVAL_HOURS, default=current
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=MIN_SCAN_INTERVAL_HOURS,
|
||||
max=MAX_SCAN_INTERVAL_HOURS,
|
||||
step=1,
|
||||
mode=NumberSelectorMode.BOX,
|
||||
unit_of_measurement="h",
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init", data_schema=data_schema, last_step=True
|
||||
)
|
||||
Reference in new issue
Block a user