"""Config flow for the Google Weather integration.""" from collections.abc import Mapping import logging from typing import Any, override from google_weather_api import GoogleWeatherApi, GoogleWeatherApiError import voluptuous as vol from homeassistant.config_entries import ( SOURCE_REAUTH, SOURCE_RECONFIGURE, SOURCE_USER, ConfigEntry, ConfigEntryState, ConfigFlow, ConfigFlowResult, ConfigSubentryFlow, SubentryFlowResult, ) from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, CONF_NAME, ) from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import section from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import LocationSelector, LocationSelectorConfig from .const import CONF_REFERRER, DOMAIN, SECTION_API_KEY_OPTIONS _LOGGER = logging.getLogger(__name__) STEP_USER_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_API_KEY): str, vol.Optional(SECTION_API_KEY_OPTIONS): section( vol.Schema({vol.Optional(CONF_REFERRER): str}), {"collapsed": True} ), } ) async def _validate_input( user_input: dict[str, Any], api: GoogleWeatherApi, errors: dict[str, str], description_placeholders: dict[str, str], ) -> bool: try: await api.async_get_current_conditions( latitude=user_input[CONF_LOCATION][CONF_LATITUDE], longitude=user_input[CONF_LOCATION][CONF_LONGITUDE], ) except GoogleWeatherApiError as err: errors["base"] = "cannot_connect" description_placeholders["error_message"] = str(err) except Exception: _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: return True return False def _get_location_schema(hass: HomeAssistant) -> vol.Schema: """Return the schema for a location with default values from the hass config.""" return vol.Schema( { # Name field is no longer allowed in config flow schemas # pylint: disable-next=home-assistant-config-flow-name-field vol.Required(CONF_NAME, default=hass.config.location_name): str, vol.Required( CONF_LOCATION, default={ CONF_LATITUDE: hass.config.latitude, CONF_LONGITUDE: hass.config.longitude, }, ): LocationSelector(LocationSelectorConfig(radius=False)), } ) def _is_location_already_configured( hass: HomeAssistant, new_data: dict[str, float], epsilon: float = 1e-4, exclude_subentry_id: str | None = None, ) -> bool: """Check if the location is already configured.""" for entry in hass.config_entries.async_entries(DOMAIN): for subentry in entry.subentries.values(): if exclude_subentry_id and subentry.subentry_id == exclude_subentry_id: continue # A more accurate way is to use the haversine formula, but for simplicity # we use a simple distance check. The epsilon value is small anyway. # This is mostly to capture cases where the user # has slightly moved the location pin. if ( abs(subentry.data[CONF_LATITUDE] - new_data[CONF_LATITUDE]) <= epsilon and abs(subentry.data[CONF_LONGITUDE] - new_data[CONF_LONGITUDE]) <= epsilon ): return True return False class GoogleWeatherConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Google Weather.""" VERSION = 1 @override async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a flow initialized by the user, reauth or reconfigure.""" errors: dict[str, str] = {} description_placeholders: dict[str, str] = { "api_key_url": "https://developers.google.com/maps/documentation/weather/get-api-key", "restricting_api_keys_url": "https://developers.google.com/maps/api-security-best-practices#restricting-api-keys", } if user_input is not None: api_key = user_input[CONF_API_KEY] referrer = user_input.get(SECTION_API_KEY_OPTIONS, {}).get(CONF_REFERRER) self._async_abort_entries_match({CONF_API_KEY: api_key}) if self.source in (SOURCE_REAUTH, SOURCE_RECONFIGURE): entry = ( self._get_reauth_entry() if self.source == SOURCE_REAUTH else self._get_reconfigure_entry() ) subentry = next(iter(entry.subentries.values()), None) if subentry: latitude = subentry.data[CONF_LATITUDE] longitude = subentry.data[CONF_LONGITUDE] else: latitude = self.hass.config.latitude longitude = self.hass.config.longitude validation_input = { CONF_LOCATION: {CONF_LATITUDE: latitude, CONF_LONGITUDE: longitude} } else: if _is_location_already_configured( self.hass, user_input[CONF_LOCATION] ): return self.async_abort(reason="already_configured") validation_input = user_input api = GoogleWeatherApi( session=async_get_clientsession(self.hass), api_key=api_key, referrer=referrer, language_code=self.hass.config.language, ) if await _validate_input( validation_input, api, errors, description_placeholders ): data = {CONF_API_KEY: api_key, CONF_REFERRER: referrer} if self.source in (SOURCE_REAUTH, SOURCE_RECONFIGURE): return self.async_update_reload_and_abort(entry, data=data) return self.async_create_entry( title="Google Weather", data=data, subentries=[ { "subentry_type": "location", "data": user_input[CONF_LOCATION], "title": user_input[CONF_NAME], "unique_id": None, }, ], ) if self.source in (SOURCE_REAUTH, SOURCE_RECONFIGURE): entry = ( self._get_reauth_entry() if self.source == SOURCE_REAUTH else self._get_reconfigure_entry() ) if user_input is None: user_input = { CONF_API_KEY: entry.data.get(CONF_API_KEY), SECTION_API_KEY_OPTIONS: { CONF_REFERRER: entry.data.get(CONF_REFERRER) }, } schema = STEP_USER_DATA_SCHEMA else: if user_input is None: user_input = {} schema_dict = STEP_USER_DATA_SCHEMA.schema.copy() schema_dict.update(_get_location_schema(self.hass).schema) schema = vol.Schema(schema_dict) return self.async_show_form( step_id="user", data_schema=self.add_suggested_values_to_schema(schema, user_input), errors=errors, description_placeholders=description_placeholders, ) async def async_step_reauth( self, entry_data: Mapping[str, Any] ) -> ConfigFlowResult: """Handle reauth flow.""" return await self.async_step_user() async def async_step_reconfigure( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle a reconfiguration flow.""" return await self.async_step_user(user_input) @classmethod @callback @override def async_get_supported_subentry_types( cls, config_entry: ConfigEntry ) -> dict[str, type[ConfigSubentryFlow]]: """Return subentries supported by this integration.""" return {"location": LocationSubentryFlowHandler} class LocationSubentryFlowHandler(ConfigSubentryFlow): """Handle a subentry flow for location.""" @property def _is_new(self) -> bool: """Return if this is a new subentry.""" return self.source == SOURCE_USER async def async_step_location( self, user_input: dict[str, Any] | None = None, ) -> SubentryFlowResult: """Handle the location step.""" if self._get_entry().state is not ConfigEntryState.LOADED: return self.async_abort(reason="entry_not_loaded") errors: dict[str, str] = {} description_placeholders: dict[str, str] = {} if user_input is not None: exclude_id = ( None if self._is_new else self._get_reconfigure_subentry().subentry_id ) if _is_location_already_configured( self.hass, user_input[CONF_LOCATION], exclude_subentry_id=exclude_id ): return self.async_abort(reason="already_configured") api: GoogleWeatherApi = self._get_entry().runtime_data.api if await _validate_input(user_input, api, errors, description_placeholders): if self._is_new: return self.async_create_entry( title=user_input[CONF_NAME], data=user_input[CONF_LOCATION], ) return self.async_update_and_abort( self._get_entry(), self._get_reconfigure_subentry(), title=user_input[CONF_NAME], data=user_input[CONF_LOCATION], ) elif self._is_new: user_input = {} else: subentry = self._get_reconfigure_subentry() user_input = { CONF_NAME: subentry.title, CONF_LOCATION: dict(subentry.data), } return self.async_show_form( step_id="location", data_schema=self.add_suggested_values_to_schema( _get_location_schema(self.hass), user_input ), errors=errors, description_placeholders=description_placeholders, ) async_step_user = async_step_location async_step_reconfigure = async_step_location