Files
core/homeassistant/components/environment_canada/config_flow.py
T

327 lines
12 KiB
Python

"""Config flow for Environment Canada integration."""
import logging
from typing import Any, override
import xml.etree.ElementTree as ET
import aiohttp
from env_canada import ECWeather, ec_exc
from env_canada.ec_weather import get_ec_sites_list
import voluptuous as vol
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import callback
from homeassistant.data_entry_flow import section
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.selector import (
BooleanSelector,
NumberSelector,
NumberSelectorConfig,
NumberSelectorMode,
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from .const import (
CONF_RADAR_COLORS,
CONF_RADAR_DURATION,
CONF_RADAR_FPS,
CONF_RADAR_FUTURE_MINUTES,
CONF_RADAR_INTERPOLATION,
CONF_RADAR_LAYER,
CONF_RADAR_LEGEND,
CONF_RADAR_OPACITY,
CONF_RADAR_RADIUS,
CONF_RADAR_TIMESTAMP,
CONF_STATION,
CONF_TITLE,
DEFAULT_RADAR_COLORS,
DEFAULT_RADAR_DURATION,
DEFAULT_RADAR_FPS,
DEFAULT_RADAR_FUTURE_MINUTES,
DEFAULT_RADAR_INTERPOLATION,
DEFAULT_RADAR_LAYER,
DEFAULT_RADAR_LEGEND,
DEFAULT_RADAR_OPACITY,
DEFAULT_RADAR_RADIUS,
DEFAULT_RADAR_TIMESTAMP,
DOMAIN,
RADAR_COLOR_OPTIONS,
RADAR_LAYERS,
SECTION_IMAGE,
SECTION_MAP,
SECTION_RADAR,
SECTION_TIME,
)
_LOGGER = logging.getLogger(__name__)
async def validate_input(data):
"""Validate the user input allows us to connect."""
lat = data.get(CONF_LATITUDE)
lon = data.get(CONF_LONGITUDE)
station = data.get(CONF_STATION)
lang = data.get(CONF_LANGUAGE).lower()
if station:
# When station is provided, use it and get the coordinates from ECWeather
weather_data = ECWeather(station_id=station, language=lang)
await weather_data.update()
# Always use the station's coordinates, not the user-provided ones
lat = weather_data.lat
lon = weather_data.lon
else:
# When no station is provided, use coordinates to find nearest station
weather_data = ECWeather(coordinates=(lat, lon), language=lang)
await weather_data.update()
return {
CONF_TITLE: weather_data.metadata.location,
CONF_STATION: weather_data.station_id,
CONF_LATITUDE: lat,
CONF_LONGITUDE: lon,
}
class EnvironmentCanadaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Environment Canada weather."""
VERSION = 1
_station_codes: list[dict[str, str]] | None = None
@staticmethod
@callback
@override
def async_get_options_flow(
config_entry: ConfigEntry,
) -> OptionsFlowHandler:
"""Return the options flow handler."""
return OptionsFlowHandler()
async def _get_station_codes(self) -> list[dict[str, str]]:
"""Get station codes, cached after first call."""
if self._station_codes is None:
self._station_codes = await get_ec_sites_list()
return self._station_codes
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors = {}
if user_input is not None:
try:
info = await validate_input(user_input)
except ET.ParseError, vol.MultipleInvalid, ec_exc.UnknownStationId:
errors["base"] = "bad_station_id"
except aiohttp.ClientConnectionError:
errors["base"] = "cannot_connect"
except aiohttp.ClientResponseError as err:
if err.status == 404:
errors["base"] = "bad_station_id"
else:
errors["base"] = "error_response"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if not errors:
user_input[CONF_STATION] = info[CONF_STATION]
user_input[CONF_LATITUDE] = info[CONF_LATITUDE]
user_input[CONF_LONGITUDE] = info[CONF_LONGITUDE]
# The combination of station and language are
# unique for all EC weather reporting
await self.async_set_unique_id(
f"{user_input[CONF_STATION]}-{user_input[CONF_LANGUAGE].lower()}"
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info[CONF_TITLE], data=user_input)
station_codes = await self._get_station_codes()
data_schema = vol.Schema(
{
vol.Optional(CONF_STATION): SelectSelector(
SelectSelectorConfig(
options=[
SelectOptionDict(
value=station["value"], label=station["label"]
)
for station in station_codes
],
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Optional(
CONF_LATITUDE, default=self.hass.config.latitude
): cv.latitude,
vol.Optional(
CONF_LONGITUDE, default=self.hass.config.longitude
): cv.longitude,
vol.Required(CONF_LANGUAGE, default="English"): vol.In(
["English", "French"]
),
}
)
return self.async_show_form(
step_id="user", data_schema=data_schema, errors=errors
)
class OptionsFlowHandler(OptionsFlowWithReload):
"""Handle Environment Canada radar camera options."""
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the radar camera options."""
if user_input is not None:
flat_options = {
**user_input[SECTION_MAP],
**user_input[SECTION_RADAR],
**user_input[SECTION_TIME],
**user_input[SECTION_IMAGE],
}
return self.async_create_entry(data=flat_options)
options = self.config_entry.options
data_schema = vol.Schema(
{
vol.Required(SECTION_MAP): section(
vol.Schema(
{
vol.Required(
CONF_RADAR_RADIUS,
default=options.get(
CONF_RADAR_RADIUS, DEFAULT_RADAR_RADIUS
),
): NumberSelector(
NumberSelectorConfig(
min=10, max=2000, step=10, unit_of_measurement="km"
)
),
}
),
{"collapsed": False},
),
vol.Required(SECTION_RADAR): section(
vol.Schema(
{
vol.Required(
CONF_RADAR_LAYER,
default=options.get(
CONF_RADAR_LAYER, DEFAULT_RADAR_LAYER
),
): SelectSelector(
SelectSelectorConfig(
options=RADAR_LAYERS,
translation_key="radar_layer",
)
),
vol.Required(
CONF_RADAR_COLORS,
default=options.get(
CONF_RADAR_COLORS, DEFAULT_RADAR_COLORS
),
): SelectSelector(
SelectSelectorConfig(
options=RADAR_COLOR_OPTIONS,
translation_key="radar_colors",
)
),
vol.Required(
CONF_RADAR_OPACITY,
default=options.get(
CONF_RADAR_OPACITY, DEFAULT_RADAR_OPACITY
),
): NumberSelector(
NumberSelectorConfig(
min=0,
max=100,
step=1,
mode=NumberSelectorMode.SLIDER,
)
),
vol.Required(
CONF_RADAR_LEGEND,
default=options.get(
CONF_RADAR_LEGEND, DEFAULT_RADAR_LEGEND
),
): BooleanSelector(),
}
),
{"collapsed": False},
),
vol.Required(SECTION_TIME): section(
vol.Schema(
{
vol.Required(
CONF_RADAR_DURATION,
default=options.get(
CONF_RADAR_DURATION, DEFAULT_RADAR_DURATION
),
): NumberSelector(
NumberSelectorConfig(
min=0, max=180, step=5, unit_of_measurement="min"
)
),
vol.Required(
CONF_RADAR_FUTURE_MINUTES,
default=options.get(
CONF_RADAR_FUTURE_MINUTES,
DEFAULT_RADAR_FUTURE_MINUTES,
),
): NumberSelector(
NumberSelectorConfig(
min=0, max=72, step=1, unit_of_measurement="min"
)
),
vol.Required(
CONF_RADAR_TIMESTAMP,
default=options.get(
CONF_RADAR_TIMESTAMP, DEFAULT_RADAR_TIMESTAMP
),
): BooleanSelector(),
}
),
{"collapsed": False},
),
vol.Required(SECTION_IMAGE): section(
vol.Schema(
{
vol.Required(
CONF_RADAR_INTERPOLATION,
default=options.get(
CONF_RADAR_INTERPOLATION,
DEFAULT_RADAR_INTERPOLATION,
),
): BooleanSelector(),
vol.Required(
CONF_RADAR_FPS,
default=options.get(CONF_RADAR_FPS, DEFAULT_RADAR_FPS),
): NumberSelector(
NumberSelectorConfig(
min=1, max=30, step=1, unit_of_measurement="fps"
)
),
}
),
{"collapsed": True},
),
}
)
return self.async_show_form(step_id="init", data_schema=data_schema)