278 files

This commit is contained in:
Home Assistant Version Control committed 2026-09-13 20:38:58 +00:00
1 parent d7a0a08372
commit 1b4f5f68c6
278 files changed
+36892 -7150

No files matched your search

+151 -29
View File
@@ -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
)