"""Config flow for SolarEdge Optimizers.""" from __future__ import annotations from typing import Any, override import voluptuous as vol from homeassistant.config_entries import ( ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow, ) from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ObjectSelector from homeassistant.helpers.service_info.hassio import HassioServiceInfo from .api import ( OptimizerSnapshot, SolarEdgeOptimizerApiClient, SolarEdgeOptimizerConnectionError, SolarEdgeOptimizerInvalidResponseError, normalize_base_url, ) from .areas import ( AreaMappingError, default_area_mapping, parse_area_mapping, serialize_area_mapping, ) from .const import ( CONF_AREA_MAPPING, CONF_SCAN_INTERVAL, CONFIG_ENTRY_VERSION, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, MIN_SCAN_INTERVAL_SECONDS, ) async def validate_input( hass: HomeAssistant, data: dict[str, Any] ) -> tuple[str, OptimizerSnapshot]: """Validate user input and return the normalized URL and snapshot.""" url = normalize_base_url(data[CONF_URL]) client = SolarEdgeOptimizerApiClient(url, async_get_clientsession(hass)) return url, await client.async_get_optimizers() def _schema(defaults: dict[str, Any]) -> vol.Schema: """Return the manual and reconfigure schema.""" url_marker: vol.Marker if CONF_URL in defaults: url_marker = vol.Required(CONF_URL, default=defaults[CONF_URL]) else: url_marker = vol.Required(CONF_URL) return vol.Schema( { url_marker: str, vol.Required( CONF_SCAN_INTERVAL, default=defaults.get( CONF_SCAN_INTERVAL, int(DEFAULT_SCAN_INTERVAL.total_seconds()), ), ): vol.All(vol.Coerce(int), vol.Range(min=MIN_SCAN_INTERVAL_SECONDS)), } ) class SolarEdgeOptimizersConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for SolarEdge Optimizers.""" VERSION = CONFIG_ENTRY_VERSION @staticmethod @callback @override def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: """Return the options flow.""" return SolarEdgeOptimizerOptionsFlow() def __init__(self) -> None: """Initialize the flow.""" self._discovered_url: str | None = None self._discovered_name: str | None = None @override async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle manual setup.""" return await self._async_handle_form("user", user_input) @override async def async_step_hassio( self, discovery_info: HassioServiceInfo ) -> ConfigFlowResult: """Handle discovery from the Home Assistant App.""" host = discovery_info.config.get("host") port = discovery_info.config.get("port", DEFAULT_PORT) if not isinstance(host, str) or not host: return self.async_abort(reason="invalid_discovery") try: port = int(port) self._discovered_url = normalize_base_url(f"http://{host}:{port}") except TypeError, ValueError: return self.async_abort(reason="invalid_discovery") self._discovered_name = discovery_info.name self.context["title_placeholders"] = {"name": discovery_info.name} return await self.async_step_hassio_confirm() async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Confirm App discovery.""" if user_input is None: self._abort_if_unique_id_configured() return self.async_show_form( step_id="hassio_confirm", description_placeholders={"name": self._discovered_name or "App"}, ) assert self._discovered_url is not None data = { CONF_URL: self._discovered_url, CONF_SCAN_INTERVAL: int(DEFAULT_SCAN_INTERVAL.total_seconds()), } try: url, snapshot = await validate_input(self.hass, data) except SolarEdgeOptimizerConnectionError: return self.async_abort(reason="cannot_connect") except SolarEdgeOptimizerInvalidResponseError: return self.async_abort(reason="invalid_response") except Exception: # noqa: BLE001 LOGGER.exception("Unexpected exception during App discovery") return self.async_abort(reason="unknown") await self.async_set_unique_id(snapshot.site_id) self._abort_if_unique_id_configured(updates={CONF_URL: url}) return self.async_create_entry( title=f"SolarEdge Site {snapshot.site_id}", data={**data, CONF_URL: url} ) @override async def async_step_reconfigure( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Reconfigure the App connection.""" entry = self._get_reconfigure_entry() if user_input is None: return self.async_show_form( step_id="reconfigure", data_schema=_schema(dict(entry.data)) ) result = await self._async_validate_form(user_input) if isinstance(result, dict): return self.async_show_form( step_id="reconfigure", data_schema=_schema(user_input), errors=result, ) url, snapshot = result if entry.unique_id != snapshot.site_id: return self.async_show_form( step_id="reconfigure", data_schema=_schema(user_input), errors={"base": "site_id_mismatch"}, ) return self.async_update_reload_and_abort( entry, data_updates={**user_input, CONF_URL: url}, ) async def _async_handle_form( self, step_id: str, user_input: dict[str, Any] | None ) -> ConfigFlowResult: if user_input is None: return self.async_show_form(step_id=step_id, data_schema=_schema({})) result = await self._async_validate_form(user_input) if isinstance(result, dict): return self.async_show_form( step_id=step_id, data_schema=_schema(user_input), errors=result, ) url, snapshot = result await self.async_set_unique_id(snapshot.site_id) self._abort_if_unique_id_configured(updates={CONF_URL: url}) return self.async_create_entry( title=f"SolarEdge Site {snapshot.site_id}", data={**user_input, CONF_URL: url}, ) async def _async_validate_form( self, user_input: dict[str, Any] ) -> tuple[str, OptimizerSnapshot] | dict[str, str]: try: return await validate_input(self.hass, user_input) except ValueError: return {"base": "invalid_url"} except SolarEdgeOptimizerConnectionError: return {"base": "cannot_connect"} except SolarEdgeOptimizerInvalidResponseError: return {"base": "invalid_response"} except Exception: # noqa: BLE001 LOGGER.exception("Unexpected exception while validating configuration") return {"base": "unknown"} class SolarEdgeOptimizerOptionsFlow(OptionsFlow): """Configure optimizer area mappings.""" @override async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Manage area mapping options.""" coordinator = self.config_entry.runtime_data available_optimizer_ids = { optimizer.optimizer_id for optimizer in coordinator.data.optimizers if optimizer.optimizer_id is not None } errors: dict[str, str] = {} if user_input is not None: try: mapping = parse_area_mapping( user_input.get(CONF_AREA_MAPPING, {}), available_optimizer_ids, ) except AreaMappingError as err: errors["base"] = err.translation_key else: return self.async_create_entry( data={ **self.config_entry.options, CONF_AREA_MAPPING: serialize_area_mapping(mapping), } ) site_id = coordinator.data.site_id current_mapping = self.config_entry.options.get( CONF_AREA_MAPPING, default_area_mapping(site_id) ) if user_input is not None: current_mapping = user_input.get(CONF_AREA_MAPPING, {}) return self.async_show_form( step_id="init", data_schema=vol.Schema( { vol.Required( CONF_AREA_MAPPING, default=current_mapping, ): ObjectSelector() } ), errors=errors, description_placeholders={ "optimizer_ids": ", ".join(sorted(available_optimizer_ids)) }, )