diff --git a/homeassistant/components/airos/config_flow.py b/homeassistant/components/airos/config_flow.py index f0e4b48a8cc067..fac4ccef804c53 100644 --- a/homeassistant/components/airos/config_flow.py +++ b/homeassistant/components/airos/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -14,7 +15,7 @@ ) import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -24,6 +25,11 @@ ) from homeassistant.data_entry_flow import section from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.selector import ( + TextSelector, + TextSelectorConfig, + TextSelectorType, +) from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS from .coordinator import AirOS8 @@ -54,50 +60,107 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 MINOR_VERSION = 2 + def __init__(self) -> None: + """Initialize the config flow.""" + super().__init__() + self.airos_device: AirOS8 + self.errors: dict[str, str] = {} + async def async_step_user( - self, - user_input: dict[str, Any] | None = None, + self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Handle the initial step.""" - errors: dict[str, str] = {} + """Handle the manual input of host and credentials.""" + self.errors = {} if user_input is not None: - # By default airOS 8 comes with self-signed SSL certificates, - # with no option in the web UI to change or upload a custom certificate. - session = async_get_clientsession( - self.hass, - verify_ssl=user_input[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL], - ) - - airos_device = AirOS8( - host=user_input[CONF_HOST], - username=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], - session=session, - use_ssl=user_input[SECTION_ADVANCED_SETTINGS][CONF_SSL], - ) - try: - await airos_device.login() - airos_data = await airos_device.status() - - except ( - AirOSConnectionSetupError, - AirOSDeviceConnectionError, - ): - errors["base"] = "cannot_connect" - except (AirOSConnectionAuthenticationError, AirOSDataMissingError): - errors["base"] = "invalid_auth" - except AirOSKeyDataMissingError: - errors["base"] = "key_data_missing" - except Exception: - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" + validated_info = await self._validate_and_get_device_info(user_input) + if validated_info: + return self.async_create_entry( + title=validated_info["title"], + data=validated_info["data"], + ) + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=self.errors + ) + + async def _validate_and_get_device_info( + self, config_data: dict[str, Any] + ) -> dict[str, Any] | None: + """Validate user input with the device API.""" + # By default airOS 8 comes with self-signed SSL certificates, + # with no option in the web UI to change or upload a custom certificate. + session = async_get_clientsession( + self.hass, + verify_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL], + ) + + airos_device = AirOS8( + host=config_data[CONF_HOST], + username=config_data[CONF_USERNAME], + password=config_data[CONF_PASSWORD], + session=session, + use_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_SSL], + ) + try: + await airos_device.login() + airos_data = await airos_device.status() + + except ( + AirOSConnectionSetupError, + AirOSDeviceConnectionError, + ): + self.errors["base"] = "cannot_connect" + except (AirOSConnectionAuthenticationError, AirOSDataMissingError): + self.errors["base"] = "invalid_auth" + except AirOSKeyDataMissingError: + self.errors["base"] = "key_data_missing" + except Exception: + _LOGGER.exception("Unexpected exception during credential validation") + self.errors["base"] = "unknown" + else: + await self.async_set_unique_id(airos_data.derived.mac) + + if self.source == SOURCE_REAUTH: + self._abort_if_unique_id_mismatch() else: - await self.async_set_unique_id(airos_data.derived.mac) self._abort_if_unique_id_configured() - return self.async_create_entry( - title=airos_data.host.hostname, data=user_input + + return {"title": airos_data.host.hostname, "data": config_data} + + return None + + async def async_step_reauth( + self, + user_input: Mapping[str, Any], + ) -> ConfigFlowResult: + """Perform reauthentication upon an API authentication error.""" + return await self.async_step_reauth_confirm(user_input) + + async def async_step_reauth_confirm( + self, + user_input: Mapping[str, Any], + ) -> ConfigFlowResult: + """Perform reauthentication upon an API authentication error.""" + self.errors = {} + + if user_input: + validate_data = {**self._get_reauth_entry().data, **user_input} + if await self._validate_and_get_device_info(config_data=validate_data): + return self.async_update_reload_and_abort( + self._get_reauth_entry(), + data_updates=validate_data, ) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): TextSelector( + TextSelectorConfig( + type=TextSelectorType.PASSWORD, + autocomplete="current-password", + ) + ), + } + ), + errors=self.errors, ) diff --git a/homeassistant/components/airos/coordinator.py b/homeassistant/components/airos/coordinator.py index 68f7256f352c82..b1f9a770c0aece 100644 --- a/homeassistant/components/airos/coordinator.py +++ b/homeassistant/components/airos/coordinator.py @@ -14,7 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, SCAN_INTERVAL @@ -47,9 +47,9 @@ async def _async_update_data(self) -> AirOS8Data: try: await self.airos_device.login() return await self.airos_device.status() - except (AirOSConnectionAuthenticationError,) as err: + except AirOSConnectionAuthenticationError as err: _LOGGER.exception("Error authenticating with airOS device") - raise ConfigEntryError( + raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="invalid_auth" ) from err except ( diff --git a/homeassistant/components/airos/strings.json b/homeassistant/components/airos/strings.json index a6e83aae8692b4..8630ee8c7af8f7 100644 --- a/homeassistant/components/airos/strings.json +++ b/homeassistant/components/airos/strings.json @@ -2,6 +2,14 @@ "config": { "flow_title": "Ubiquiti airOS device", "step": { + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "password": "[%key:component::airos::config::step::user::data_description::password%]" + } + }, "user": { "data": { "host": "[%key:common::config_flow::data::host%]", @@ -34,7 +42,9 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "unique_id_mismatch": "Re-authentication should be used for the same device not a new one" } }, "entity": { diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index 3d4c62ee1c7faa..a27dee9fcb1d2a 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -32,6 +32,7 @@ def set_source_entity_id_or_uuid(source_entity_id: str) -> None: entry, options={**entry.options, CONF_SOURCE: source_entity_id}, ) + hass.config_entries.async_schedule_reload(entry.entry_id) entry.async_on_unload( async_handle_source_entity_changes( @@ -46,15 +47,9 @@ def set_source_entity_id_or_uuid(source_entity_id: str) -> None: ) ) await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) - entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True -async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update listener, called when the config entry options are changed.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, (Platform.SENSOR,)) diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py index be371837442c86..f9014681088aba 100644 --- a/homeassistant/components/derivative/config_flow.py +++ b/homeassistant/components/derivative/config_flow.py @@ -140,6 +140,7 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True VERSION = 1 MINOR_VERSION = 4 diff --git a/homeassistant/components/filter/__init__.py b/homeassistant/components/filter/__init__.py index 9a4f4913c9f29e..8d7a39b128061a 100644 --- a/homeassistant/components/filter/__init__.py +++ b/homeassistant/components/filter/__init__.py @@ -10,7 +10,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Filter from a config entry.""" await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(update_listener)) return True @@ -18,8 +17,3 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Filter config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - -async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle options update.""" - await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/filter/config_flow.py b/homeassistant/components/filter/config_flow.py index 7bbfb9f6f0a517..f974250b1e81b6 100644 --- a/homeassistant/components/filter/config_flow.py +++ b/homeassistant/components/filter/config_flow.py @@ -246,6 +246,7 @@ class FilterConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" diff --git a/homeassistant/components/generic_thermostat/__init__.py b/homeassistant/components/generic_thermostat/__init__.py index 98cd9a02baa54b..177f6695bac4ee 100644 --- a/homeassistant/components/generic_thermostat/__init__.py +++ b/homeassistant/components/generic_thermostat/__init__.py @@ -35,6 +35,7 @@ def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None: entry, options={**entry.options, CONF_HEATER: source_entity_id}, ) + hass.config_entries.async_schedule_reload(entry.entry_id) entry.async_on_unload( # We use async_handle_source_entity_changes to track changes to the heater, but @@ -67,6 +68,7 @@ async def async_sensor_updated( entry, options={**entry.options, CONF_SENSOR: data["entity_id"]}, ) + hass.config_entries.async_schedule_reload(entry.entry_id) entry.async_on_unload( async_track_entity_registry_updated_event( @@ -75,7 +77,6 @@ async def async_sensor_updated( ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True @@ -113,11 +114,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update listener, called when the config entry options are changed.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/generic_thermostat/config_flow.py b/homeassistant/components/generic_thermostat/config_flow.py index b69106597d12cb..c1045cad536e6a 100644 --- a/homeassistant/components/generic_thermostat/config_flow.py +++ b/homeassistant/components/generic_thermostat/config_flow.py @@ -104,6 +104,7 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index c48cd8529a20c1..f64979c6a66543 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -141,15 +141,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups( entry, (entry.options["group_type"],) ) - entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True -async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update listener, called when the config entry options are changed.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms( diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 88f7d9017ab9e3..0433deab8ae77c 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -329,6 +329,7 @@ class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True @callback def async_config_entry_title(self, options: Mapping[str, Any]) -> str: diff --git a/homeassistant/components/history_stats/__init__.py b/homeassistant/components/history_stats/__init__.py index efddabd180cec2..87efcf274bd34c 100644 --- a/homeassistant/components/history_stats/__init__.py +++ b/homeassistant/components/history_stats/__init__.py @@ -65,6 +65,7 @@ def set_source_entity_id_or_uuid(source_entity_id: str) -> None: entry, options={**entry.options, CONF_ENTITY_ID: source_entity_id}, ) + hass.config_entries.async_schedule_reload(entry.entry_id) async def source_entity_removed() -> None: # The source entity has been removed, we remove the config entry because @@ -86,7 +87,6 @@ async def source_entity_removed() -> None: ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(update_listener)) return True @@ -130,8 +130,3 @@ async def async_unload_entry( ) -> bool: """Unload History stats config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - -async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle options update.""" - await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/history_stats/config_flow.py b/homeassistant/components/history_stats/config_flow.py index e8c3be8aef5668..84232ef8873fd8 100644 --- a/homeassistant/components/history_stats/config_flow.py +++ b/homeassistant/components/history_stats/config_flow.py @@ -162,6 +162,7 @@ class HistoryStatsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" diff --git a/homeassistant/components/mold_indicator/__init__.py b/homeassistant/components/mold_indicator/__init__.py index e252338d4d862c..372947f04c478e 100644 --- a/homeassistant/components/mold_indicator/__init__.py +++ b/homeassistant/components/mold_indicator/__init__.py @@ -39,6 +39,7 @@ def set_source_entity_id_or_uuid(source_entity_id: str) -> None: entry, options={**entry.options, CONF_INDOOR_HUMIDITY: source_entity_id}, ) + hass.config_entries.async_schedule_reload(entry.entry_id) entry.async_on_unload( # We use async_handle_source_entity_changes to track changes to the humidity @@ -79,6 +80,7 @@ def async_sensor_updated( entry, options={**entry.options, temp_sensor: data["entity_id"]}, ) + hass.config_entries.async_schedule_reload(entry.entry_id) return async_sensor_updated @@ -89,7 +91,6 @@ def async_sensor_updated( ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(update_listener)) return True @@ -99,11 +100,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle options update.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate old entry.""" diff --git a/homeassistant/components/mold_indicator/config_flow.py b/homeassistant/components/mold_indicator/config_flow.py index d370752fff97e4..9d8a95c4716429 100644 --- a/homeassistant/components/mold_indicator/config_flow.py +++ b/homeassistant/components/mold_indicator/config_flow.py @@ -100,6 +100,7 @@ class MoldIndicatorConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True VERSION = 1 MINOR_VERSION = 2 diff --git a/homeassistant/components/random/__init__.py b/homeassistant/components/random/__init__.py index bff2ce53dfbbe1..28569e49c26b4a 100644 --- a/homeassistant/components/random/__init__.py +++ b/homeassistant/components/random/__init__.py @@ -9,15 +9,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups( entry, (entry.options["entity_type"],) ) - entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True -async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update listener, called when the config entry options are changed.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms( diff --git a/homeassistant/components/random/config_flow.py b/homeassistant/components/random/config_flow.py index 406100388e639d..c709b75f490896 100644 --- a/homeassistant/components/random/config_flow.py +++ b/homeassistant/components/random/config_flow.py @@ -184,6 +184,7 @@ class RandomConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True @callback def async_config_entry_title(self, options: Mapping[str, Any]) -> str: diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index b511e2af2b2346..dfb5ded2791277 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -52,6 +52,7 @@ def set_source_entity_id_or_uuid(source_entity_id: str) -> None: entry, options={**entry.options, CONF_ENTITY_ID: source_entity_id}, ) + hass.config_entries.async_schedule_reload(entry.entry_id) async def source_entity_removed() -> None: # The source entity has been removed, we remove the config entry because @@ -69,7 +70,6 @@ async def source_entity_removed() -> None: source_entity_removed=source_entity_removed, ) ) - entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) await hass.config_entries.async_forward_entry_setups( entry, (entry.options[CONF_TARGET_DOMAIN],) @@ -113,11 +113,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update listener, called when the config entry options are changed.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms( diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index cf442256cbe5be..4b44af63234f7d 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -56,6 +56,7 @@ class SwitchAsXConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True VERSION = 1 MINOR_VERSION = 3 diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index c3f832b0c54ad4..5a07a2c7255136 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -102,15 +102,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups( entry, (entry.options["template_type"],) ) - entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True -async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update listener, called when the config entry options are changed.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms( diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index aa9c6a8f2c0809..15ed1ed2126c9d 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -695,6 +695,7 @@ class TemplateConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True @callback def async_config_entry_title(self, options: Mapping[str, Any]) -> str: diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 8a388058b19030..a79881d3983a45 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -228,6 +228,7 @@ def set_source_entity_id_or_uuid(source_entity_id: str) -> None: entry, options={**entry.options, CONF_SOURCE_SENSOR: source_entity_id}, ) + hass.config_entries.async_schedule_reload(entry.entry_id) entry.async_on_unload( async_handle_source_entity_changes( @@ -258,17 +259,9 @@ def set_source_entity_id_or_uuid(source_entity_id: str) -> None: entry, (Platform.SELECT, Platform.SENSOR) ) - entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) - return True -async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update listener, called when the config entry options are changed.""" - - await hass.config_entries.async_reload(entry.entry_id) - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" platforms_to_unload = [Platform.SENSOR] diff --git a/homeassistant/components/utility_meter/config_flow.py b/homeassistant/components/utility_meter/config_flow.py index 933a04accbac27..06706c7921664d 100644 --- a/homeassistant/components/utility_meter/config_flow.py +++ b/homeassistant/components/utility_meter/config_flow.py @@ -134,6 +134,7 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" diff --git a/homeassistant/components/worldclock/__init__.py b/homeassistant/components/worldclock/__init__.py index ad01c45917a3fc..c9bd5aa1e2ee57 100644 --- a/homeassistant/components/worldclock/__init__.py +++ b/homeassistant/components/worldclock/__init__.py @@ -10,7 +10,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Worldclock from a config entry.""" await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_on_unload(entry.add_update_listener(update_listener)) return True @@ -18,8 +17,3 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload World clock config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - -async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle options update.""" - await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/worldclock/config_flow.py b/homeassistant/components/worldclock/config_flow.py index e91d2e40f63d71..f248d5de4c6aab 100644 --- a/homeassistant/components/worldclock/config_flow.py +++ b/homeassistant/components/worldclock/config_flow.py @@ -97,6 +97,7 @@ class WorldclockConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): config_flow = CONFIG_FLOW options_flow = OPTIONS_FLOW + options_flow_reloads = True def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" diff --git a/tests/components/airos/test_config_flow.py b/tests/components/airos/test_config_flow.py index a502f9f2f3bc25..6b5c6f47716152 100644 --- a/tests/components/airos/test_config_flow.py +++ b/tests/components/airos/test_config_flow.py @@ -24,6 +24,9 @@ from tests.common import MockConfigEntry +NEW_PASSWORD = "new_password" +REAUTH_STEP = "reauth_confirm" + MOCK_CONFIG = { CONF_HOST: "1.1.1.1", CONF_USERNAME: "ubnt", @@ -33,6 +36,11 @@ CONF_VERIFY_SSL: False, }, } +MOCK_CONFIG_REAUTH = { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "ubnt", + CONF_PASSWORD: "wrong-password", +} async def test_form_creates_entry( @@ -89,7 +97,6 @@ async def test_form_duplicate_entry( @pytest.mark.parametrize( ("exception", "error"), [ - (AirOSConnectionAuthenticationError, "invalid_auth"), (AirOSDeviceConnectionError, "cannot_connect"), (AirOSKeyDataMissingError, "key_data_missing"), (Exception, "unknown"), @@ -128,3 +135,95 @@ async def test_form_exception_handling( assert result["title"] == "NanoStation 5AC ap name" assert result["data"] == MOCK_CONFIG assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("reauth_exception", "expected_error"), + [ + (None, None), + (AirOSConnectionAuthenticationError, "invalid_auth"), + (AirOSDeviceConnectionError, "cannot_connect"), + (AirOSKeyDataMissingError, "key_data_missing"), + (Exception, "unknown"), + ], + ids=[ + "reauth_succes", + "invalid_auth", + "cannot_connect", + "key_data_missing", + "unknown", + ], +) +async def test_reauth_flow_scenarios( + hass: HomeAssistant, + mock_airos_client: AsyncMock, + mock_config_entry: MockConfigEntry, + reauth_exception: Exception, + expected_error: str, +) -> None: + """Test reauthentication from start (failure) to finish (success).""" + mock_config_entry.add_to_hass(hass) + + mock_airos_client.login.side_effect = AirOSConnectionAuthenticationError + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == REAUTH_STEP + + mock_airos_client.login.side_effect = reauth_exception + + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], + user_input={CONF_PASSWORD: NEW_PASSWORD}, + ) + + if expected_error: + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == REAUTH_STEP + assert result["errors"] == {"base": expected_error} + + # Retry + mock_airos_client.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], + user_input={CONF_PASSWORD: NEW_PASSWORD}, + ) + + # Always test resolution + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert updated_entry.data[CONF_PASSWORD] == NEW_PASSWORD + + +async def test_reauth_unique_id_mismatch( + hass: HomeAssistant, + mock_airos_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauthentication failure when the unique ID changes.""" + mock_config_entry.add_to_hass(hass) + + mock_airos_client.login.side_effect = AirOSConnectionAuthenticationError + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + flows = hass.config_entries.flow.async_progress() + flow = flows[0] + + mock_airos_client.login.side_effect = None + mock_airos_client.status.return_value.derived.mac = "FF:23:45:67:89:AB" + + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], + user_input={CONF_PASSWORD: NEW_PASSWORD}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "unique_id_mismatch" + + updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert updated_entry.data[CONF_PASSWORD] != NEW_PASSWORD diff --git a/tests/components/airos/test_sensor.py b/tests/components/airos/test_sensor.py index 7f39f504753688..2e30a181905e70 100644 --- a/tests/components/airos/test_sensor.py +++ b/tests/components/airos/test_sensor.py @@ -3,11 +3,7 @@ from datetime import timedelta from unittest.mock import AsyncMock -from airos.exceptions import ( - AirOSConnectionAuthenticationError, - AirOSDataMissingError, - AirOSDeviceConnectionError, -) +from airos.exceptions import AirOSDataMissingError, AirOSDeviceConnectionError from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion @@ -39,7 +35,6 @@ async def test_all_entities( @pytest.mark.parametrize( ("exception"), [ - AirOSConnectionAuthenticationError, TimeoutError, AirOSDeviceConnectionError, AirOSDataMissingError,