|
|
@@ -0,0 +1,202 @@
|
|
|
+"""Config flow for Integration 101 Template integration."""
|
|
|
+
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import logging
|
|
|
+from typing import Any
|
|
|
+
|
|
|
+import voluptuous as vol
|
|
|
+
|
|
|
+from homeassistant.config_entries import (
|
|
|
+ ConfigEntry,
|
|
|
+ ConfigFlow,
|
|
|
+ ConfigFlowResult,
|
|
|
+ OptionsFlow,
|
|
|
+)
|
|
|
+from homeassistant.const import (
|
|
|
+ CONF_HOST,
|
|
|
+ CONF_PASSWORD,
|
|
|
+ CONF_SCAN_INTERVAL,
|
|
|
+ CONF_USERNAME,
|
|
|
+)
|
|
|
+from homeassistant.core import HomeAssistant, callback
|
|
|
+from homeassistant.exceptions import HomeAssistantError
|
|
|
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
|
+
|
|
|
+from .api import (RouterAPI,
|
|
|
+ RouterAPIAuthError,
|
|
|
+ RouterAPIConnectionError,
|
|
|
+ RouterAPIInvalidResponse)
|
|
|
+from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MIN_SCAN_INTERVAL
|
|
|
+
|
|
|
+_LOGGER = logging.getLogger(__name__)
|
|
|
+
|
|
|
+# TODO adjust the data schema to the data that you need
|
|
|
+STEP_USER_DATA_SCHEMA = vol.Schema(
|
|
|
+ {
|
|
|
+ vol.Required(CONF_HOST, description={"suggested_value": "192.168.1.1"}): str,
|
|
|
+ vol.Required(CONF_USERNAME, description={"suggested_value": "admin"}): str,
|
|
|
+ vol.Required(CONF_PASSWORD, description={"suggested_value": "*****"}): str,
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
|
|
+ """Validate the user input allows us to connect.
|
|
|
+
|
|
|
+ Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
|
|
+ """
|
|
|
+ # TODO validate the data can be used to set up a connection.
|
|
|
+
|
|
|
+ # If your PyPI package is not built with async, pass your methods
|
|
|
+ # to the executor:
|
|
|
+ # await hass.async_add_executor_job(
|
|
|
+ # your_validate_func, data[CONF_USERNAME], data[CONF_PASSWORD]
|
|
|
+ # )
|
|
|
+
|
|
|
+ session = async_get_clientsession(
|
|
|
+ hass=hass,
|
|
|
+ verify_ssl=False
|
|
|
+ )
|
|
|
+
|
|
|
+ api = RouterAPI(host=data[CONF_HOST],
|
|
|
+ user=data[CONF_USERNAME],
|
|
|
+ pwd=data[CONF_PASSWORD],
|
|
|
+ session=session)
|
|
|
+ try:
|
|
|
+ await api.async_login()
|
|
|
+ except RouterAPIAuthError as err:
|
|
|
+ raise InvalidAuth from err
|
|
|
+ except RouterAPIConnectionError as err:
|
|
|
+ raise CannotConnect from err
|
|
|
+
|
|
|
+ return {"title": f"Odido Klik & Klaar router - {data[CONF_HOST]}"}
|
|
|
+
|
|
|
+
|
|
|
+class RouterConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
+ """Handle a config flow for Example Integration."""
|
|
|
+
|
|
|
+ VERSION = 1
|
|
|
+ _input_data: dict[str, Any]
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ @callback
|
|
|
+ def async_get_options_flow(config_entry):
|
|
|
+ """Get the options flow for this handler."""
|
|
|
+ # Remove this method and the ExampleOptionsFlowHandler class
|
|
|
+ # if you do not want any options for your integration.
|
|
|
+ return RouterOptionsFlowHandler(config_entry)
|
|
|
+
|
|
|
+ async def async_step_user(
|
|
|
+ self, user_input: dict[str, Any] | None = None
|
|
|
+ ) -> ConfigFlowResult:
|
|
|
+ """Handle the initial step."""
|
|
|
+ # Called when you initiate adding an integration via the UI
|
|
|
+ errors: dict[str, str] = {}
|
|
|
+
|
|
|
+ if user_input is not None:
|
|
|
+ # The form has been filled in and submitted, so process the data provided.
|
|
|
+ try:
|
|
|
+ # Validate that the setup data is valid and if not handle errors.
|
|
|
+ # The errors["base"] values match the values in your strings.json and translation files.
|
|
|
+ info = await validate_input(self.hass, user_input)
|
|
|
+ except CannotConnect:
|
|
|
+ errors["base"] = "cannot_connect"
|
|
|
+ except InvalidAuth:
|
|
|
+ errors["base"] = "invalid_auth"
|
|
|
+ except Exception: # pylint: disable=broad-except
|
|
|
+ _LOGGER.exception("Unexpected exception")
|
|
|
+ errors["base"] = "unknown"
|
|
|
+
|
|
|
+ if "base" not in errors:
|
|
|
+ # Validation was successful, so create a unique id for this instance of your integration
|
|
|
+ # and create the config entry.
|
|
|
+ await self.async_set_unique_id(info.get("title"))
|
|
|
+ self._abort_if_unique_id_configured()
|
|
|
+ return self.async_create_entry(title=info["title"], data=user_input)
|
|
|
+
|
|
|
+ # Show initial form.
|
|
|
+ return self.async_show_form(
|
|
|
+ step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
|
|
+ )
|
|
|
+
|
|
|
+ async def async_step_reconfigure(
|
|
|
+ self, user_input: dict[str, Any] | None = None
|
|
|
+ ) -> ConfigFlowResult:
|
|
|
+ """Add reconfigure step to allow to reconfigure a config entry."""
|
|
|
+ # This methid displays a reconfigure option in the integration and is
|
|
|
+ # different to options.
|
|
|
+ # It can be used to reconfigure any of the data submitted when first installed.
|
|
|
+ # This is optional and can be removed if you do not want to allow reconfiguration.
|
|
|
+ errors: dict[str, str] = {}
|
|
|
+ config_entry = self.hass.config_entries.async_get_entry(
|
|
|
+ self.context["entry_id"]
|
|
|
+ )
|
|
|
+
|
|
|
+ if user_input is not None:
|
|
|
+ try:
|
|
|
+ user_input[CONF_HOST] = config_entry.data[CONF_HOST]
|
|
|
+ await validate_input(self.hass, user_input)
|
|
|
+ except CannotConnect:
|
|
|
+ errors["base"] = "cannot_connect"
|
|
|
+ except InvalidAuth:
|
|
|
+ errors["base"] = "invalid_auth"
|
|
|
+ except Exception: # pylint: disable=broad-except
|
|
|
+ _LOGGER.exception("Unexpected exception")
|
|
|
+ errors["base"] = "unknown"
|
|
|
+ else:
|
|
|
+ return self.async_update_reload_and_abort(
|
|
|
+ config_entry,
|
|
|
+ unique_id=config_entry.unique_id,
|
|
|
+ data={**config_entry.data, **user_input},
|
|
|
+ reason="reconfigure_successful",
|
|
|
+ )
|
|
|
+ return self.async_show_form(
|
|
|
+ step_id="reconfigure",
|
|
|
+ data_schema=vol.Schema(
|
|
|
+ {
|
|
|
+ vol.Required(
|
|
|
+ CONF_USERNAME, default=config_entry.data[CONF_USERNAME]
|
|
|
+ ): str,
|
|
|
+ vol.Required(CONF_PASSWORD): str,
|
|
|
+ }
|
|
|
+ ),
|
|
|
+ errors=errors,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+class RouterOptionsFlowHandler(OptionsFlow):
|
|
|
+ """Handles the options flow."""
|
|
|
+
|
|
|
+ def __init__(self, config_entry: ConfigEntry) -> None:
|
|
|
+ """Initialize options flow."""
|
|
|
+ self.config_entry = config_entry
|
|
|
+ self.options = dict(config_entry.options)
|
|
|
+
|
|
|
+ async def async_step_init(self, user_input=None):
|
|
|
+ """Handle options flow."""
|
|
|
+ if user_input is not None:
|
|
|
+ options = self.config_entry.options | user_input
|
|
|
+ return self.async_create_entry(title="", data=options)
|
|
|
+
|
|
|
+ # It is recommended to prepopulate options fields with default values if available.
|
|
|
+ # These will be the same default values you use on your coordinator for setting variable values
|
|
|
+ # if the option has not been set.
|
|
|
+ data_schema = vol.Schema(
|
|
|
+ {
|
|
|
+ vol.Required(
|
|
|
+ CONF_SCAN_INTERVAL,
|
|
|
+ default=self.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL),
|
|
|
+ ): (vol.All(vol.Coerce(int), vol.Clamp(min=MIN_SCAN_INTERVAL))),
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ return self.async_show_form(step_id="init", data_schema=data_schema)
|
|
|
+
|
|
|
+
|
|
|
+class CannotConnect(HomeAssistantError):
|
|
|
+ """Error to indicate we cannot connect."""
|
|
|
+
|
|
|
+
|
|
|
+class InvalidAuth(HomeAssistantError):
|
|
|
+ """Error to indicate there is invalid auth."""
|