From b5874a15e85bdabac38d987089f6cfd27ff1bc74 Mon Sep 17 00:00:00 2001 From: Davide Caputo Date: Fri, 2 Oct 2026 11:00:57 +0100 Subject: [PATCH] Reload retrying Music Assistant entry when the server is rediscovered (#183579) --- .../components/music_assistant/config_flow.py | 10 +++ .../music_assistant/test_config_flow.py | 85 +++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/homeassistant/components/music_assistant/config_flow.py b/homeassistant/components/music_assistant/config_flow.py index 8c6cae7b6358..447cd8eccd47 100644 --- a/homeassistant/components/music_assistant/config_flow.py +++ b/homeassistant/components/music_assistant/config_flow.py @@ -180,6 +180,10 @@ class MusicAssistantConfigFlow(ConfigFlow, domain=DOMAIN): ConfigEntryState.SETUP_IN_PROGRESS, ): self.hass.config_entries.async_schedule_reload(entry.entry_id) + elif entry.state is ConfigEntryState.SETUP_RETRY: + # The server answered, so it is back online: retry setup now + # instead of waiting for the next backoff interval + self.hass.config_entries.async_schedule_reload(entry.entry_id) # Abort since entry already exists return self.async_abort(reason="already_configured") @@ -220,6 +224,12 @@ class MusicAssistantConfigFlow(ConfigFlow, domain=DOMAIN): # Ignore servers running as Home Assistant app # (they should be discovered through hassio discovery instead) if server_info.homeassistant_addon: + # The app server announcing itself means it is online, so an + # existing entry waiting to retry setup is reloaded now + await self.async_set_unique_id( + server_info.server_id, raise_on_progress=False + ) + self._abort_if_unique_id_configured() LOGGER.debug("Ignoring HA app server in zeroconf discovery") return self.async_abort(reason="already_discovered_addon") diff --git a/tests/components/music_assistant/test_config_flow.py b/tests/components/music_assistant/test_config_flow.py index f716eabe7b22..6c8926d94cc3 100644 --- a/tests/components/music_assistant/test_config_flow.py +++ b/tests/components/music_assistant/test_config_flow.py @@ -516,6 +516,48 @@ async def test_hassio_flow_updates_failed_entry_and_reloads( mock_schedule_reload.assert_called_once_with(failed_entry.entry_id) +@pytest.mark.parametrize( + ("entry_state", "reload_expected"), + [ + (ConfigEntryState.SETUP_RETRY, True), + (ConfigEntryState.LOADED, False), + (ConfigEntryState.NOT_LOADED, False), + ], +) +async def test_hassio_flow_unchanged_entry_reloads_only_when_retrying( + hass: HomeAssistant, + mock_get_server_info: AsyncMock, + entry_state: ConfigEntryState, + reload_expected: bool, +) -> None: + """Test hassio discovery with unchanged data reloads only a retrying entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Music Assistant", + data={CONF_URL: "http://addon-music-assistant:8094", CONF_TOKEN: "test_token"}, + unique_id="1234", + ) + entry.add_to_hass(hass) + entry.mock_state(hass, entry_state) + + with patch.object( + hass.config_entries, "async_schedule_reload" + ) as mock_schedule_reload: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_HASSIO}, + data=HASSIO_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + if reload_expected: + mock_schedule_reload.assert_called_once_with(entry.entry_id) + else: + mock_schedule_reload.assert_not_called() + + @pytest.mark.parametrize( ("exception", "error_reason"), [ @@ -565,6 +607,49 @@ async def test_zeroconf_addon_server_ignored( assert result["reason"] == "already_discovered_addon" +@pytest.mark.parametrize( + ("entry_state", "reload_expected"), + [ + (ConfigEntryState.SETUP_RETRY, True), + (ConfigEntryState.LOADED, False), + ], +) +async def test_zeroconf_addon_server_reloads_retrying_entry( + hass: HomeAssistant, + mock_get_server_info: AsyncMock, + mock_config_entry: MockConfigEntry, + entry_state: ConfigEntryState, + reload_expected: bool, +) -> None: + """Test zeroconf discovery of an add-on server reloads a retrying entry.""" + mock_config_entry.add_to_hass(hass) + mock_config_entry.mock_state(hass, entry_state) + addon_zeroconf_data = deepcopy(ZEROCONF_DATA) + addon_zeroconf_data.properties["homeassistant_addon"] = ( + "True" # Zeroconf properties are strings + ) + + with patch.object( + hass.config_entries, "async_schedule_reload" + ) as mock_schedule_reload: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=addon_zeroconf_data, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + # The add-on entry keeps its own URL, not the zeroconf base_url + assert mock_config_entry.data == {CONF_URL: "http://localhost:8095"} + mock_get_server_info.assert_not_called() + if reload_expected: + mock_schedule_reload.assert_called_once_with(mock_config_entry.entry_id) + else: + mock_schedule_reload.assert_not_called() + + async def test_zeroconf_old_schema_addon_not_ignored( hass: HomeAssistant, mock_get_server_info: AsyncMock,