From 0164470dcadccc8903832afb79122024fe8f0174 Mon Sep 17 00:00:00 2001 From: Jeff Stein Date: Mon, 4 May 2026 07:27:56 -0600 Subject: [PATCH 1/2] Fix IntelliFire setup recovery --- .../components/intellifire/__init__.py | 5 ++ .../components/intellifire/config_flow.py | 58 ++++++++++--------- .../components/intellifire/strings.json | 3 + .../intellifire/test_config_flow.py | 29 ++++++++++ tests/components/intellifire/test_init.py | 20 +++++-- 5 files changed, 81 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index 3f2f693b198cc7..a2e3fe6b425731 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -2,6 +2,7 @@ import asyncio +import aiohttp from intellifire4py import UnifiedFireplace from intellifire4py.cloud_interface import IntelliFireCloudInterface from intellifire4py.const import IntelliFireApiMode @@ -153,6 +154,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: IntellifireConfigEntry) raise ConfigEntryNotReady( "Initialization of fireplace timed out after 10 minutes" ) from err + except (aiohttp.ClientConnectionError, ConnectionError) as err: + raise ConfigEntryNotReady( + "Error communicating with fireplace during initialization" + ) from err # Construct coordinator data_update_coordinator = IntellifireDataUpdateCoordinator(hass, entry, fireplace) diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py index 95b1ee0c1d8eea..c1de3a34cb5b98 100644 --- a/homeassistant/components/intellifire/config_flow.py +++ b/homeassistant/components/intellifire/config_flow.py @@ -287,34 +287,36 @@ async def async_step_init( errors: dict[str, str] = {} if user_input is not None: - # Validate connectivity for requested modes if runtime data is available - coordinator = self.config_entry.runtime_data - if coordinator is not None: - fireplace = coordinator.fireplace - - # Refresh connectivity status before validating - await fireplace.async_validate_connectivity() - - if ( - user_input[CONF_READ_MODE] == API_MODE_LOCAL - and not fireplace.local_connectivity - ): - errors[CONF_READ_MODE] = "local_unavailable" - if ( - user_input[CONF_READ_MODE] == API_MODE_CLOUD - and not fireplace.cloud_connectivity - ): - errors[CONF_READ_MODE] = "cloud_unavailable" - if ( - user_input[CONF_CONTROL_MODE] == API_MODE_LOCAL - and not fireplace.local_connectivity - ): - errors[CONF_CONTROL_MODE] = "local_unavailable" - if ( - user_input[CONF_CONTROL_MODE] == API_MODE_CLOUD - and not fireplace.cloud_connectivity - ): - errors[CONF_CONTROL_MODE] = "cloud_unavailable" + if ( + coordinator := getattr(self.config_entry, "runtime_data", None) + ) is None: + return self.async_abort(reason="not_loaded") + + fireplace = coordinator.fireplace + + # Refresh connectivity status before validating + await fireplace.async_validate_connectivity() + + if ( + user_input[CONF_READ_MODE] == API_MODE_LOCAL + and not fireplace.local_connectivity + ): + errors[CONF_READ_MODE] = "local_unavailable" + if ( + user_input[CONF_READ_MODE] == API_MODE_CLOUD + and not fireplace.cloud_connectivity + ): + errors[CONF_READ_MODE] = "cloud_unavailable" + if ( + user_input[CONF_CONTROL_MODE] == API_MODE_LOCAL + and not fireplace.local_connectivity + ): + errors[CONF_CONTROL_MODE] = "local_unavailable" + if ( + user_input[CONF_CONTROL_MODE] == API_MODE_CLOUD + and not fireplace.cloud_connectivity + ): + errors[CONF_CONTROL_MODE] = "cloud_unavailable" if not errors: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/intellifire/strings.json b/homeassistant/components/intellifire/strings.json index 3faca975f0122c..759054e0c59d80 100644 --- a/homeassistant/components/intellifire/strings.json +++ b/homeassistant/components/intellifire/strings.json @@ -155,6 +155,9 @@ } }, "options": { + "abort": { + "not_loaded": "The IntelliFire integration is not loaded." + }, "error": { "cloud_unavailable": "Cloud connectivity is not available", "local_unavailable": "Local connectivity is not available" diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index 20223d255e67da..0ad0f64f955db7 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -272,6 +272,35 @@ async def test_options_flow( } +async def test_options_flow_not_loaded_on_submit( + hass: HomeAssistant, + mock_config_entry_current: MockConfigEntry, +) -> None: + """Test options flow aborts when runtime data is missing on submit.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + version=mock_config_entry_current.version, + minor_version=mock_config_entry_current.minor_version, + data=dict(mock_config_entry_current.data), + options=dict(mock_config_entry_current.options), + unique_id=mock_config_entry_current.unique_id, + state=config_entries.ConfigEntryState.SETUP_ERROR, + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + {CONF_READ_MODE: API_MODE_CLOUD, CONF_CONTROL_MODE: API_MODE_LOCAL}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "not_loaded" + + async def test_options_flow_local_read_unavailable( hass: HomeAssistant, mock_config_entry_current: MockConfigEntry, diff --git a/tests/components/intellifire/test_init.py b/tests/components/intellifire/test_init.py index ac689a164b5bab..f71681c81e6fef 100644 --- a/tests/components/intellifire/test_init.py +++ b/tests/components/intellifire/test_init.py @@ -1,8 +1,10 @@ """Test the IntelliFire config flow.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch +import aiohttp from intellifire4py.const import IntelliFireApiMode +import pytest from homeassistant.components.intellifire import CONF_USER_ID from homeassistant.components.intellifire.const import ( @@ -159,22 +161,28 @@ async def test_init_with_no_username(hass: HomeAssistant, mock_apis_single_fp) - assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR -async def test_connectivity_bad( +@pytest.mark.parametrize( + "setup_error", + [aiohttp.ClientConnectionError, ConnectionError, TimeoutError], +) +async def test_connectivity_error_during_setup_retries( hass: HomeAssistant, - mock_config_entry_current, - mock_apis_single_fp, + mock_config_entry_current: MockConfigEntry, + mock_apis_single_fp: tuple[AsyncMock, AsyncMock, MagicMock], + setup_error: type[Exception], ) -> None: - """Test a timeout error on the setup flow.""" + """Test a connection error during setup retries the config entry.""" with patch( "homeassistant.components.intellifire.UnifiedFireplace.build_fireplace_from_common", new_callable=AsyncMock, - side_effect=TimeoutError, + side_effect=setup_error, ): mock_config_entry_current.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry_current.entry_id) await hass.async_block_till_done() + assert mock_config_entry_current.state is ConfigEntryState.SETUP_RETRY assert len(hass.states.async_all()) == 0 From 7815666f906978eb9161bdbee85b8bb0c0ca842c Mon Sep 17 00:00:00 2001 From: Jeff Stein Date: Wed, 6 May 2026 13:16:45 -0600 Subject: [PATCH 2/2] addressing PR comments --- .../components/intellifire/config_flow.py | 57 +++++++++---------- .../components/intellifire/strings.json | 3 - .../intellifire/test_config_flow.py | 11 ++-- 3 files changed, 34 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py index c1de3a34cb5b98..403daf4ab5194a 100644 --- a/homeassistant/components/intellifire/config_flow.py +++ b/homeassistant/components/intellifire/config_flow.py @@ -13,6 +13,7 @@ from homeassistant.config_entries import ( SOURCE_REAUTH, + ConfigEntryState, ConfigFlow, ConfigFlowResult, OptionsFlow, @@ -287,36 +288,32 @@ async def async_step_init( errors: dict[str, str] = {} if user_input is not None: - if ( - coordinator := getattr(self.config_entry, "runtime_data", None) - ) is None: - return self.async_abort(reason="not_loaded") - - fireplace = coordinator.fireplace - - # Refresh connectivity status before validating - await fireplace.async_validate_connectivity() - - if ( - user_input[CONF_READ_MODE] == API_MODE_LOCAL - and not fireplace.local_connectivity - ): - errors[CONF_READ_MODE] = "local_unavailable" - if ( - user_input[CONF_READ_MODE] == API_MODE_CLOUD - and not fireplace.cloud_connectivity - ): - errors[CONF_READ_MODE] = "cloud_unavailable" - if ( - user_input[CONF_CONTROL_MODE] == API_MODE_LOCAL - and not fireplace.local_connectivity - ): - errors[CONF_CONTROL_MODE] = "local_unavailable" - if ( - user_input[CONF_CONTROL_MODE] == API_MODE_CLOUD - and not fireplace.cloud_connectivity - ): - errors[CONF_CONTROL_MODE] = "cloud_unavailable" + if self.config_entry.state is ConfigEntryState.LOADED: + fireplace = self.config_entry.runtime_data.fireplace + + # Refresh connectivity status before validating + await fireplace.async_validate_connectivity() + + if ( + user_input[CONF_READ_MODE] == API_MODE_LOCAL + and not fireplace.local_connectivity + ): + errors[CONF_READ_MODE] = "local_unavailable" + if ( + user_input[CONF_READ_MODE] == API_MODE_CLOUD + and not fireplace.cloud_connectivity + ): + errors[CONF_READ_MODE] = "cloud_unavailable" + if ( + user_input[CONF_CONTROL_MODE] == API_MODE_LOCAL + and not fireplace.local_connectivity + ): + errors[CONF_CONTROL_MODE] = "local_unavailable" + if ( + user_input[CONF_CONTROL_MODE] == API_MODE_CLOUD + and not fireplace.cloud_connectivity + ): + errors[CONF_CONTROL_MODE] = "cloud_unavailable" if not errors: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/intellifire/strings.json b/homeassistant/components/intellifire/strings.json index 759054e0c59d80..3faca975f0122c 100644 --- a/homeassistant/components/intellifire/strings.json +++ b/homeassistant/components/intellifire/strings.json @@ -155,9 +155,6 @@ } }, "options": { - "abort": { - "not_loaded": "The IntelliFire integration is not loaded." - }, "error": { "cloud_unavailable": "Cloud connectivity is not available", "local_unavailable": "Local connectivity is not available" diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index 0ad0f64f955db7..2d10873748a218 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -272,11 +272,11 @@ async def test_options_flow( } -async def test_options_flow_not_loaded_on_submit( +async def test_options_flow_allows_submit_when_not_loaded( hass: HomeAssistant, mock_config_entry_current: MockConfigEntry, ) -> None: - """Test options flow aborts when runtime data is missing on submit.""" + """Test options flow allows submit when runtime data is missing.""" config_entry = MockConfigEntry( domain=DOMAIN, version=mock_config_entry_current.version, @@ -297,8 +297,11 @@ async def test_options_flow_not_loaded_on_submit( {CONF_READ_MODE: API_MODE_CLOUD, CONF_CONTROL_MODE: API_MODE_LOCAL}, ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "not_loaded" + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_READ_MODE: API_MODE_CLOUD, + CONF_CONTROL_MODE: API_MODE_LOCAL, + } async def test_options_flow_local_read_unavailable(