From 1a05780358e05de752eec7d7619e1fbb11393f11 Mon Sep 17 00:00:00 2001 From: Jeef Date: Wed, 11 Mar 2026 11:56:24 -0600 Subject: [PATCH 01/10] double polling --- .../components/intellifire/__init__.py | 3 +- .../components/intellifire/coordinator.py | 1 + tests/components/intellifire/conftest.py | 2 + tests/components/intellifire/test_init.py | 50 +++++++++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index 8a32515212034..77171044e9b9a 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -143,7 +143,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: IntellifireConfigEntry) try: fireplace: UnifiedFireplace = ( await UnifiedFireplace.build_fireplace_from_common( - _construct_common_data(entry) + _construct_common_data(entry), + polling_enabled=False, ) ) LOGGER.debug("Waiting for Fireplace to Initialize") diff --git a/homeassistant/components/intellifire/coordinator.py b/homeassistant/components/intellifire/coordinator.py index dc9aa45d58bcd..fd61d3501c97c 100644 --- a/homeassistant/components/intellifire/coordinator.py +++ b/homeassistant/components/intellifire/coordinator.py @@ -52,6 +52,7 @@ def control_api(self) -> IntelliFireController: return self.fireplace.control_api async def _async_update_data(self) -> IntelliFirePollData: + await self.fireplace.perform_poll() return self.fireplace.data @property diff --git a/tests/components/intellifire/conftest.py b/tests/components/intellifire/conftest.py index a82deba64ee92..008e1db9fc3b4 100644 --- a/tests/components/intellifire/conftest.py +++ b/tests/components/intellifire/conftest.py @@ -257,6 +257,8 @@ def mock_fp(mock_common_data_local) -> Generator[AsyncMock]: mock_instance.set_read_mode = AsyncMock() mock_instance.set_control_mode = AsyncMock() + mock_instance.perform_poll = AsyncMock() + mock_instance.async_validate_connectivity = AsyncMock( return_value=(True, False) ) diff --git a/tests/components/intellifire/test_init.py b/tests/components/intellifire/test_init.py index 307a9df812c50..435b5b175e484 100644 --- a/tests/components/intellifire/test_init.py +++ b/tests/components/intellifire/test_init.py @@ -342,3 +342,53 @@ async def test_update_options_no_change( mock_fp.set_control_mode.assert_not_called() # But async_request_refresh should still be called coordinator.async_request_refresh.assert_called_once() + + +async def test_coordinator_performs_poll( + hass: HomeAssistant, + mock_config_entry_current: MockConfigEntry, + mock_apis_single_fp, +) -> None: + """Test that the coordinator uses perform_poll() for data refresh. + + This verifies the double-polling fix: instead of the library polling + automatically AND Home Assistant polling on its schedule, we disable + the library's auto-polling and have HA explicitly call perform_poll() + when it wants fresh data. + """ + _mock_local, _mock_cloud, mock_fp = mock_apis_single_fp + + 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() + + # Verify perform_poll was called during initial setup/refresh + mock_fp.perform_poll.assert_called() + + +async def test_fireplace_built_with_polling_disabled( + hass: HomeAssistant, + mock_config_entry_current: MockConfigEntry, + mock_apis_single_fp, +) -> None: + """Test that the fireplace is built with polling_enabled=False. + + This is the other half of the double-polling fix: we tell the library + not to auto-poll, so Home Assistant can control polling via perform_poll(). + """ + _mock_local, _mock_cloud, mock_fp = mock_apis_single_fp + + # We need to capture the call to build_fireplace_from_common + with patch( + "homeassistant.components.intellifire.UnifiedFireplace.build_fireplace_from_common", + new_callable=AsyncMock, + return_value=mock_fp, + ) as mock_build: + 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() + + # Verify build_fireplace_from_common was called with polling_enabled=False + mock_build.assert_called_once() + call_kwargs = mock_build.call_args.kwargs + assert call_kwargs.get("polling_enabled") is False From 81ff8417b30a4108e6a3af473e33f4873324ff8b Mon Sep 17 00:00:00 2001 From: Jeef Date: Wed, 11 Mar 2026 14:13:29 -0600 Subject: [PATCH 02/10] address PR comments --- tests/components/intellifire/test_init.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/components/intellifire/test_init.py b/tests/components/intellifire/test_init.py index 435b5b175e484..7484d8f7d5b17 100644 --- a/tests/components/intellifire/test_init.py +++ b/tests/components/intellifire/test_init.py @@ -349,12 +349,10 @@ async def test_coordinator_performs_poll( mock_config_entry_current: MockConfigEntry, mock_apis_single_fp, ) -> None: - """Test that the coordinator uses perform_poll() for data refresh. + """Test that the library only polls when instructed by the coordinator. - This verifies the double-polling fix: instead of the library polling - automatically AND Home Assistant polling on its schedule, we disable - the library's auto-polling and have HA explicitly call perform_poll() - when it wants fresh data. + The library auto-polls by default; ensure the coordinator disables that + and drives polling explicitly via perform_poll(). """ _mock_local, _mock_cloud, mock_fp = mock_apis_single_fp @@ -363,7 +361,7 @@ async def test_coordinator_performs_poll( await hass.async_block_till_done() # Verify perform_poll was called during initial setup/refresh - mock_fp.perform_poll.assert_called() + mock_fp.perform_poll.assert_called_once() async def test_fireplace_built_with_polling_disabled( From 49640da6bbec66d159aede4d17727526a0b423e7 Mon Sep 17 00:00:00 2001 From: Jeef Date: Wed, 11 Mar 2026 14:17:16 -0600 Subject: [PATCH 03/10] Fix test docstring to avoid referencing the PR context Co-Authored-By: Claude Sonnet 4.6 --- tests/components/intellifire/test_init.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/intellifire/test_init.py b/tests/components/intellifire/test_init.py index 7484d8f7d5b17..1ae6a81e98605 100644 --- a/tests/components/intellifire/test_init.py +++ b/tests/components/intellifire/test_init.py @@ -371,8 +371,8 @@ async def test_fireplace_built_with_polling_disabled( ) -> None: """Test that the fireplace is built with polling_enabled=False. - This is the other half of the double-polling fix: we tell the library - not to auto-poll, so Home Assistant can control polling via perform_poll(). + The library auto-polls by default; ensure it is constructed with polling + disabled so the coordinator controls all polling via perform_poll(). """ _mock_local, _mock_cloud, mock_fp = mock_apis_single_fp From b12449be985d7ce2f64bb249dc9f68276fdb1b8b Mon Sep 17 00:00:00 2001 From: Jeef Date: Wed, 11 Mar 2026 14:23:21 -0600 Subject: [PATCH 04/10] pushing changes --- tests/components/intellifire/test_init.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/components/intellifire/test_init.py b/tests/components/intellifire/test_init.py index 1ae6a81e98605..d3f8d14ee3c99 100644 --- a/tests/components/intellifire/test_init.py +++ b/tests/components/intellifire/test_init.py @@ -390,3 +390,6 @@ async def test_fireplace_built_with_polling_disabled( mock_build.assert_called_once() call_kwargs = mock_build.call_args.kwargs assert call_kwargs.get("polling_enabled") is False + # Coordinator drives exactly one poll; if the library were also auto-polling + # this would be > 1, catching the double-poll regression. + mock_fp.perform_poll.assert_called_once() From 2bfd2e865e98a5668c2e6827150d74a8b40d8adb Mon Sep 17 00:00:00 2001 From: Jeef Date: Wed, 11 Mar 2026 15:09:31 -0600 Subject: [PATCH 05/10] Use assert_awaited_once() for AsyncMock assertions in intellifire tests Co-Authored-By: Claude Sonnet 4.6 --- tests/components/intellifire/test_init.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/intellifire/test_init.py b/tests/components/intellifire/test_init.py index d3f8d14ee3c99..1b24e03bf50a6 100644 --- a/tests/components/intellifire/test_init.py +++ b/tests/components/intellifire/test_init.py @@ -360,8 +360,8 @@ async def test_coordinator_performs_poll( await hass.config_entries.async_setup(mock_config_entry_current.entry_id) await hass.async_block_till_done() - # Verify perform_poll was called during initial setup/refresh - mock_fp.perform_poll.assert_called_once() + # Verify perform_poll was awaited during initial setup/refresh + mock_fp.perform_poll.assert_awaited_once() async def test_fireplace_built_with_polling_disabled( @@ -387,9 +387,9 @@ async def test_fireplace_built_with_polling_disabled( await hass.async_block_till_done() # Verify build_fireplace_from_common was called with polling_enabled=False - mock_build.assert_called_once() + mock_build.assert_awaited_once() call_kwargs = mock_build.call_args.kwargs assert call_kwargs.get("polling_enabled") is False # Coordinator drives exactly one poll; if the library were also auto-polling # this would be > 1, catching the double-poll regression. - mock_fp.perform_poll.assert_called_once() + mock_fp.perform_poll.assert_awaited_once() From d9008683d0a022731767e6c6461c5be1be6e593b Mon Sep 17 00:00:00 2001 From: Jeef Date: Thu, 12 Mar 2026 14:35:27 -0600 Subject: [PATCH 06/10] Add signature test to guard against polling_enabled kwarg rename Co-Authored-By: Claude Sonnet 4.6 --- tests/components/intellifire/test_init.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/components/intellifire/test_init.py b/tests/components/intellifire/test_init.py index 1b24e03bf50a6..82e7cace464df 100644 --- a/tests/components/intellifire/test_init.py +++ b/tests/components/intellifire/test_init.py @@ -1,7 +1,9 @@ """Test the IntelliFire config flow.""" +import inspect from unittest.mock import AsyncMock, patch +from intellifire4py import UnifiedFireplace from intellifire4py.const import IntelliFireApiMode from homeassistant.components.intellifire import CONF_USER_ID @@ -393,3 +395,12 @@ async def test_fireplace_built_with_polling_disabled( # Coordinator drives exactly one poll; if the library were also auto-polling # this would be > 1, catching the double-poll regression. mock_fp.perform_poll.assert_awaited_once() + + +async def test_build_fireplace_from_common_accepts_polling_enabled() -> None: + """Verify the backing lib's build_fireplace_from_common accepts polling_enabled. + + Guards against the library renaming the kwarg without us noticing. + """ + sig = inspect.signature(UnifiedFireplace.build_fireplace_from_common) + assert "polling_enabled" in sig.parameters From 0bf66da2d7ab19368346a5038b5092577472fa54 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 1 Apr 2026 11:42:21 +0200 Subject: [PATCH 07/10] Update tests/components/intellifire/test_init.py --- tests/components/intellifire/test_init.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/components/intellifire/test_init.py b/tests/components/intellifire/test_init.py index 82e7cace464df..f2fc8d37744ea 100644 --- a/tests/components/intellifire/test_init.py +++ b/tests/components/intellifire/test_init.py @@ -395,12 +395,3 @@ async def test_fireplace_built_with_polling_disabled( # Coordinator drives exactly one poll; if the library were also auto-polling # this would be > 1, catching the double-poll regression. mock_fp.perform_poll.assert_awaited_once() - - -async def test_build_fireplace_from_common_accepts_polling_enabled() -> None: - """Verify the backing lib's build_fireplace_from_common accepts polling_enabled. - - Guards against the library renaming the kwarg without us noticing. - """ - sig = inspect.signature(UnifiedFireplace.build_fireplace_from_common) - assert "polling_enabled" in sig.parameters From 80a054bae3dc7838fbadcc262c14902dd1f3f91e Mon Sep 17 00:00:00 2001 From: Jeef Date: Wed, 1 Apr 2026 06:40:42 -0600 Subject: [PATCH 08/10] Update tests/components/intellifire/test_init.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/components/intellifire/test_init.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/components/intellifire/test_init.py b/tests/components/intellifire/test_init.py index f2fc8d37744ea..1531fa39e9441 100644 --- a/tests/components/intellifire/test_init.py +++ b/tests/components/intellifire/test_init.py @@ -1,11 +1,6 @@ """Test the IntelliFire config flow.""" -import inspect from unittest.mock import AsyncMock, patch - -from intellifire4py import UnifiedFireplace -from intellifire4py.const import IntelliFireApiMode - from homeassistant.components.intellifire import CONF_USER_ID from homeassistant.components.intellifire.const import ( API_MODE_CLOUD, From 564a033685d747a8a798a3207ec16531606dfb0f Mon Sep 17 00:00:00 2001 From: Jeef Date: Wed, 1 Apr 2026 06:49:11 -0600 Subject: [PATCH 09/10] addressign pr comments --- .../components/intellifire/coordinator.py | 13 +++++-- tests/components/intellifire/test_init.py | 34 ++----------------- 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/intellifire/coordinator.py b/homeassistant/components/intellifire/coordinator.py index fd61d3501c97c..c2eb374c3a14c 100644 --- a/homeassistant/components/intellifire/coordinator.py +++ b/homeassistant/components/intellifire/coordinator.py @@ -4,6 +4,7 @@ from datetime import timedelta +import aiohttp from intellifire4py import UnifiedFireplace from intellifire4py.control import IntelliFireController from intellifire4py.model import IntelliFirePollData @@ -11,8 +12,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, LOGGER @@ -52,7 +54,14 @@ def control_api(self) -> IntelliFireController: return self.fireplace.control_api async def _async_update_data(self) -> IntelliFirePollData: - await self.fireplace.perform_poll() + try: + await self.fireplace.perform_poll() + except aiohttp.ClientResponseError as err: + if err.status == 403: + raise ConfigEntryAuthFailed("Authentication failed") from err + raise UpdateFailed(f"Error communicating with fireplace: {err}") from err + except (aiohttp.ClientError, TimeoutError) as err: + raise UpdateFailed(f"Error communicating with fireplace: {err}") from err return self.fireplace.data @property diff --git a/tests/components/intellifire/test_init.py b/tests/components/intellifire/test_init.py index 1531fa39e9441..bdc4dfb00d396 100644 --- a/tests/components/intellifire/test_init.py +++ b/tests/components/intellifire/test_init.py @@ -1,6 +1,9 @@ """Test the IntelliFire config flow.""" from unittest.mock import AsyncMock, patch + +from intellifire4py.const import IntelliFireApiMode + from homeassistant.components.intellifire import CONF_USER_ID from homeassistant.components.intellifire.const import ( API_MODE_CLOUD, @@ -359,34 +362,3 @@ async def test_coordinator_performs_poll( # Verify perform_poll was awaited during initial setup/refresh mock_fp.perform_poll.assert_awaited_once() - - -async def test_fireplace_built_with_polling_disabled( - hass: HomeAssistant, - mock_config_entry_current: MockConfigEntry, - mock_apis_single_fp, -) -> None: - """Test that the fireplace is built with polling_enabled=False. - - The library auto-polls by default; ensure it is constructed with polling - disabled so the coordinator controls all polling via perform_poll(). - """ - _mock_local, _mock_cloud, mock_fp = mock_apis_single_fp - - # We need to capture the call to build_fireplace_from_common - with patch( - "homeassistant.components.intellifire.UnifiedFireplace.build_fireplace_from_common", - new_callable=AsyncMock, - return_value=mock_fp, - ) as mock_build: - 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() - - # Verify build_fireplace_from_common was called with polling_enabled=False - mock_build.assert_awaited_once() - call_kwargs = mock_build.call_args.kwargs - assert call_kwargs.get("polling_enabled") is False - # Coordinator drives exactly one poll; if the library were also auto-polling - # this would be > 1, catching the double-poll regression. - mock_fp.perform_poll.assert_awaited_once() From 758e9589807ad986a0f7a8d03379458528149457 Mon Sep 17 00:00:00 2001 From: Jeef Date: Wed, 1 Apr 2026 09:31:43 -0600 Subject: [PATCH 10/10] copilot chnages --- tests/components/intellifire/test_init.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/components/intellifire/test_init.py b/tests/components/intellifire/test_init.py index bdc4dfb00d396..ac689a164b5ba 100644 --- a/tests/components/intellifire/test_init.py +++ b/tests/components/intellifire/test_init.py @@ -356,9 +356,18 @@ async def test_coordinator_performs_poll( """ _mock_local, _mock_cloud, mock_fp = mock_apis_single_fp - 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() + with patch( + "homeassistant.components.intellifire.UnifiedFireplace.build_fireplace_from_common", + new_callable=AsyncMock, + return_value=mock_fp, + ) as mock_build: + 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() + + # Verify the fireplace was constructed with library background polling disabled + mock_build.assert_awaited_once() + assert mock_build.call_args.kwargs.get("polling_enabled") is False - # Verify perform_poll was awaited during initial setup/refresh - mock_fp.perform_poll.assert_awaited_once() + # Verify the coordinator drove exactly one poll during initial refresh + mock_fp.perform_poll.assert_awaited_once()