From b17497b436e174fe9b145635d5e439b4a02d9017 Mon Sep 17 00:00:00 2001 From: Christian Lackas Date: Thu, 27 Aug 2026 10:01:51 +0200 Subject: [PATCH] Load initial data in the background so BLE setup doesn't block startup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit async_setup_entry awaited async_config_entry_first_refresh(), so the initial BLE connect + reads (up to the 120 s connect timeout) blocked config-entry setup — HA logged "Waiting for integrations to complete setup" for a minute or more. Forward the platforms and run the first refresh in a background task instead. Entities already report unavailable until the coordinator has data, and it retries on its interval and on BLE rediscovery. Device resolution still runs in setup, so a missing device still raises ConfigEntryNotReady. test_async_setup_entry_success updated; the three setup-time refresh-error tests removed (dead path). --- .coverage | Bin 53248 -> 53248 bytes custom_components/blanco_unit/__init__.py | 37 +++---- tests/test_init.py | 118 ++-------------------- 3 files changed, 22 insertions(+), 133 deletions(-) diff --git a/.coverage b/.coverage index 3b3afea4c6070e27fc8ce145eb35dce42aaab9e5..d272a91db9377bf94beccd8344fc74021038a77f 100644 GIT binary patch delta 1212 zcmZozz}&Eac|sD?)RP-iX6Un+=@}YrGGG-@MX|_6n&Oi2geL@E9D0F?Y+n=?9(&EBaoX#tjYtMu0W6Km zSlk9QUoS>b)SabK5WDQU7!FZymPSczvPg-59e2nqh~>v)zFZuKs0T}KJlQz^ W`Q+^SUrbd+vtMr)-~6+m-2njoNtsIk delta 2633 zcmb`H&r1|x7{_;ZUvp-5=6&^2f>3&>4u-QENp#sQm>wN+wc?sC2TZ8!2P3C}$FRO_GgdF0JWVP3p=RXSI0Nlwzr< z9yipSp2*t%P>cPc9V-P)X(j|s>fn@u2t*5;T8hrxI#etOP)I$Oj%gW+9Q8sqD28Z* zJj{THl=B>NO)%Tv#R2<%h$uOh&Se15+M}jMTPxO5USa#Z7CRXBDx*7~c2EjqC07HJ zwWVVqnvK%1c|?K30AWLMLpT52v4;{tK3cMqQ;Kd%gIGqFswSupRF#HKGZdg*XSk0EOe;O42#dWn1vStbZf z>f^y+|6C?Q%@^KsnE=+G7=h|9(k5R(6AZv2sL3LAavoU*1njTK*b!tE0C-en{Q}(m zmih@)(k+eE6p%Fx5p_#r2i?liJg4!~mNO|`zMY1T!mWRV;rZKN5m z;{`m2C-Egbg3sV?aYdY^7AuJdeFQOFjI6!C$b6jYF_t!}8`YV+#Zsl=Q>8M!+8fBH zKL_r7slGGJWn=x;k*|&R>XmNhY5DL~oAFC-`ueo{LF4X*mp@K8lOCUAKA+ZAXK-nL db7*bF_5>^Y-~RfRbDUGU`SZKGT)kF%^fv(#(gpwk diff --git a/custom_components/blanco_unit/__init__.py b/custom_components/blanco_unit/__init__.py index 36c2e26..73cd008 100644 --- a/custom_components/blanco_unit/__init__.py +++ b/custom_components/blanco_unit/__init__.py @@ -20,7 +20,6 @@ from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryNotReady, - HomeAssistantError, IntegrationError, ) @@ -265,32 +264,20 @@ async def async_setup_entry( ) config_entry.runtime_data = coordinator - try: - await coordinator.async_config_entry_first_refresh() - except ConfigEntryAuthFailed as err: - # do not reload if setup failed - _LOGGER.debug("async_setup_entry ConfigEntryAuthFailed %s", str(err)) - unsub_update_listener() - raise err from err - except HomeAssistantError as err: - _LOGGER.debug("async_setup_entry HomeAssistantError %s", str(err)) - # do not reload if setup failed - unsub_update_listener() - raise ConfigEntryNotReady( - translation_key=err.translation_key, - translation_placeholders=err.translation_placeholders, - ) from err - except Exception as err: - _LOGGER.debug("async_setup_entry Exception %s", str(err)) - # do not reload if setup failed - unsub_update_listener() - raise ConfigEntryNotReady( - translation_key="error_unknown", - translation_placeholders={"error": repr(err)}, - ) from err - + # Set up the entities right away and load the first data set in the + # background. Connecting to the Blanco unit over BLE can take a long time + # or fail transiently, and blocking setup on that first refresh stalls + # Home Assistant startup. Entities report as "unavailable" until the first + # refresh succeeds; the coordinator retries on its update interval and + # whenever the device is rediscovered over Bluetooth. await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + config_entry.async_create_background_task( + hass, + coordinator.async_refresh(), + f"{DOMAIN} initial refresh {config_entry.entry_id}", + ) + return True diff --git a/tests/test_init.py b/tests/test_init.py index 01c662b..d230fab 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -33,7 +33,6 @@ from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryNotReady, - HomeAssistantError, IntegrationError, ) @@ -71,13 +70,19 @@ async def test_async_setup_entry_success(hass: HomeAssistant) -> None: mock_device.address = "AA:BB:CC:DD:EE:FF" mock_coordinator = MagicMock() - mock_coordinator.async_config_entry_first_refresh = AsyncMock() + mock_coordinator.async_refresh = AsyncMock() mock_entry = MagicMock(spec=ConfigEntry) mock_entry.entry_id = "test_entry_id" mock_entry.data = {CONF_MAC: "AA:BB:CC:DD:EE:FF"} mock_entry.add_update_listener = MagicMock(return_value=MagicMock()) + def _run_bg_task(_hass, coro, _name): + coro.close() + return MagicMock() + + mock_entry.async_create_background_task = MagicMock(side_effect=_run_bg_task) + with ( patch( "custom_components.blanco_unit.bluetooth.async_ble_device_from_address", @@ -93,7 +98,9 @@ async def test_async_setup_entry_success(hass: HomeAssistant) -> None: assert result is True assert mock_entry.runtime_data == mock_coordinator - mock_coordinator.async_config_entry_first_refresh.assert_called_once() + # The first refresh runs in the background so it cannot block startup. + mock_coordinator.async_refresh.assert_called_once() + mock_entry.async_create_background_task.assert_called_once() mock_forward.assert_called_once() # Verify platforms are registered @@ -208,111 +215,6 @@ def capture_callback(hass, callback, *args, **kwargs): mock_reload.assert_called_once_with("test_entry_id") -async def test_async_setup_entry_auth_failed(hass: HomeAssistant) -> None: - """Test config entry setup with authentication failure.""" - mock_device = MagicMock() - mock_device.address = "AA:BB:CC:DD:EE:FF" - - mock_coordinator = MagicMock() - mock_coordinator.async_config_entry_first_refresh = AsyncMock( - side_effect=ConfigEntryAuthFailed("Auth failed") - ) - - mock_entry = MagicMock(spec=ConfigEntry) - mock_entry.entry_id = "test_entry_id" - mock_entry.data = {CONF_MAC: "AA:BB:CC:DD:EE:FF"} - - unsub_listener = MagicMock() - mock_entry.add_update_listener = MagicMock(return_value=unsub_listener) - - with ( - patch( - "custom_components.blanco_unit.bluetooth.async_ble_device_from_address", - return_value=mock_device, - ), - patch( - "custom_components.blanco_unit.BlancoUnitCoordinator", - return_value=mock_coordinator, - ), - ): - with pytest.raises(ConfigEntryAuthFailed): - await async_setup_entry(hass, mock_entry) - - # Verify unsub listener was called - unsub_listener.assert_called_once() - - -async def test_async_setup_entry_home_assistant_error(hass: HomeAssistant) -> None: - """Test config entry setup with HomeAssistantError.""" - mock_device = MagicMock() - mock_device.address = "AA:BB:CC:DD:EE:FF" - - mock_coordinator = MagicMock() - error = HomeAssistantError() - error.translation_key = "test_error" - error.translation_placeholders = {"key": "value"} - mock_coordinator.async_config_entry_first_refresh = AsyncMock(side_effect=error) - - mock_entry = MagicMock(spec=ConfigEntry) - mock_entry.entry_id = "test_entry_id" - mock_entry.data = {CONF_MAC: "AA:BB:CC:DD:EE:FF"} - - unsub_listener = MagicMock() - mock_entry.add_update_listener = MagicMock(return_value=unsub_listener) - - with ( - patch( - "custom_components.blanco_unit.bluetooth.async_ble_device_from_address", - return_value=mock_device, - ), - patch( - "custom_components.blanco_unit.BlancoUnitCoordinator", - return_value=mock_coordinator, - ), - ): - with pytest.raises(ConfigEntryNotReady) as exc_info: - await async_setup_entry(hass, mock_entry) - - assert exc_info.value.translation_key == "test_error" - # Verify unsub listener was called - unsub_listener.assert_called_once() - - -async def test_async_setup_entry_generic_exception(hass: HomeAssistant) -> None: - """Test config entry setup with generic exception.""" - mock_device = MagicMock() - mock_device.address = "AA:BB:CC:DD:EE:FF" - - mock_coordinator = MagicMock() - mock_coordinator.async_config_entry_first_refresh = AsyncMock( - side_effect=ValueError("Test error") - ) - - mock_entry = MagicMock(spec=ConfigEntry) - mock_entry.entry_id = "test_entry_id" - mock_entry.data = {CONF_MAC: "AA:BB:CC:DD:EE:FF"} - - unsub_listener = MagicMock() - mock_entry.add_update_listener = MagicMock(return_value=unsub_listener) - - with ( - patch( - "custom_components.blanco_unit.bluetooth.async_ble_device_from_address", - return_value=mock_device, - ), - patch( - "custom_components.blanco_unit.BlancoUnitCoordinator", - return_value=mock_coordinator, - ), - ): - with pytest.raises(ConfigEntryNotReady) as exc_info: - await async_setup_entry(hass, mock_entry) - - assert exc_info.value.translation_key == "error_unknown" - # Verify unsub listener was called - unsub_listener.assert_called_once() - - async def test_async_reload_entry(hass: HomeAssistant) -> None: """Test config entry reload.""" mock_entry = MagicMock(spec=ConfigEntry)