Skip to content

Commit c0f61f6

Browse files
authored
Improve code quality of music assistant config flow (home-assistant#156263)
1 parent df60de3 commit c0f61f6

File tree

3 files changed

+48
-126
lines changed

3 files changed

+48
-126
lines changed

homeassistant/components/music_assistant/config_flow.py

Lines changed: 40 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,22 @@
1313
from music_assistant_models.api import ServerInfoMessage
1414
import voluptuous as vol
1515

16-
from homeassistant.config_entries import SOURCE_IGNORE, ConfigFlow, ConfigFlowResult
16+
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
1717
from homeassistant.const import CONF_URL
1818
from homeassistant.core import HomeAssistant
1919
from homeassistant.helpers import aiohttp_client
2020
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
2121

2222
from .const import DOMAIN, LOGGER
2323

24-
DEFAULT_URL = "http://mass.local:8095"
2524
DEFAULT_TITLE = "Music Assistant"
25+
DEFAULT_URL = "http://mass.local:8095"
2626

2727

28-
def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema:
29-
"""Return a schema for the manual step."""
30-
default_url = user_input.get(CONF_URL, DEFAULT_URL)
31-
return vol.Schema(
32-
{
33-
vol.Required(CONF_URL, default=default_url): str,
34-
}
35-
)
28+
STEP_USER_SCHEMA = vol.Schema({vol.Required(CONF_URL): str})
3629

3730

38-
async def get_server_info(hass: HomeAssistant, url: str) -> ServerInfoMessage:
31+
async def _get_server_info(hass: HomeAssistant, url: str) -> ServerInfoMessage:
3932
"""Validate the user input allows us to connect."""
4033
async with MusicAssistantClient(
4134
url, aiohttp_client.async_get_clientsession(hass)
@@ -52,25 +45,17 @@ class MusicAssistantConfigFlow(ConfigFlow, domain=DOMAIN):
5245

5346
def __init__(self) -> None:
5447
"""Set up flow instance."""
55-
self.server_info: ServerInfoMessage | None = None
48+
self.url: str | None = None
5649

5750
async def async_step_user(
5851
self, user_input: dict[str, Any] | None = None
5952
) -> ConfigFlowResult:
6053
"""Handle a manual configuration."""
6154
errors: dict[str, str] = {}
55+
6256
if user_input is not None:
6357
try:
64-
self.server_info = await get_server_info(
65-
self.hass, user_input[CONF_URL]
66-
)
67-
await self.async_set_unique_id(
68-
self.server_info.server_id, raise_on_progress=False
69-
)
70-
self._abort_if_unique_id_configured(
71-
updates={CONF_URL: user_input[CONF_URL]},
72-
reload_on_update=True,
73-
)
58+
server_info = await _get_server_info(self.hass, user_input[CONF_URL])
7459
except CannotConnect:
7560
errors["base"] = "cannot_connect"
7661
except InvalidServerVersion:
@@ -79,85 +64,66 @@ async def async_step_user(
7964
LOGGER.exception("Unexpected exception")
8065
errors["base"] = "unknown"
8166
else:
67+
await self.async_set_unique_id(
68+
server_info.server_id, raise_on_progress=False
69+
)
70+
self._abort_if_unique_id_configured(
71+
updates={CONF_URL: user_input[CONF_URL]}
72+
)
73+
8274
return self.async_create_entry(
8375
title=DEFAULT_TITLE,
84-
data={
85-
CONF_URL: user_input[CONF_URL],
86-
},
76+
data={CONF_URL: user_input[CONF_URL]},
8777
)
8878

89-
return self.async_show_form(
90-
step_id="user", data_schema=get_manual_schema(user_input), errors=errors
91-
)
79+
suggested_values = user_input
80+
if suggested_values is None:
81+
suggested_values = {CONF_URL: DEFAULT_URL}
9282

93-
return self.async_show_form(step_id="user", data_schema=get_manual_schema({}))
83+
return self.async_show_form(
84+
step_id="user",
85+
data_schema=self.add_suggested_values_to_schema(
86+
STEP_USER_SCHEMA, suggested_values
87+
),
88+
errors=errors,
89+
)
9490

9591
async def async_step_zeroconf(
9692
self, discovery_info: ZeroconfServiceInfo
9793
) -> ConfigFlowResult:
98-
"""Handle a discovered Mass server.
99-
100-
This flow is triggered by the Zeroconf component. It will check if the
101-
host is already configured and delegate to the import step if not.
102-
"""
103-
# abort if discovery info is not what we expect
104-
if "server_id" not in discovery_info.properties:
105-
return self.async_abort(reason="missing_server_id")
106-
107-
self.server_info = ServerInfoMessage.from_dict(discovery_info.properties)
108-
await self.async_set_unique_id(self.server_info.server_id)
94+
"""Handle a zeroconf discovery for a Music Assistant server."""
95+
try:
96+
server_info = ServerInfoMessage.from_dict(discovery_info.properties)
97+
except LookupError:
98+
return self.async_abort(reason="invalid_discovery_info")
10999

110-
# Check if we already have a config entry for this server_id
111-
existing_entry = self.hass.config_entries.async_entry_for_domain_unique_id(
112-
DOMAIN, self.server_info.server_id
113-
)
100+
self.url = server_info.base_url
114101

115-
if existing_entry:
116-
# If the entry was ignored or disabled, don't make any changes
117-
if existing_entry.source == SOURCE_IGNORE or existing_entry.disabled_by:
118-
return self.async_abort(reason="already_configured")
102+
await self.async_set_unique_id(server_info.server_id)
103+
self._abort_if_unique_id_configured(updates={CONF_URL: self.url})
119104

120-
# Test connectivity to the current URL first
121-
current_url = existing_entry.data[CONF_URL]
122-
try:
123-
await get_server_info(self.hass, current_url)
124-
# Current URL is working, no need to update
125-
return self.async_abort(reason="already_configured")
126-
except CannotConnect:
127-
# Current URL is not working, update to the discovered URL
128-
# and continue to discovery confirm
129-
self.hass.config_entries.async_update_entry(
130-
existing_entry,
131-
data={**existing_entry.data, CONF_URL: self.server_info.base_url},
132-
)
133-
# Schedule reload since URL changed
134-
self.hass.config_entries.async_schedule_reload(existing_entry.entry_id)
135-
else:
136-
# No existing entry, proceed with normal flow
137-
self._abort_if_unique_id_configured()
138-
139-
# Test connectivity to the discovered URL
140105
try:
141-
await get_server_info(self.hass, self.server_info.base_url)
106+
await _get_server_info(self.hass, self.url)
142107
except CannotConnect:
143108
return self.async_abort(reason="cannot_connect")
109+
144110
return await self.async_step_discovery_confirm()
145111

146112
async def async_step_discovery_confirm(
147113
self, user_input: dict[str, Any] | None = None
148114
) -> ConfigFlowResult:
149115
"""Handle user-confirmation of discovered server."""
150116
if TYPE_CHECKING:
151-
assert self.server_info is not None
117+
assert self.url is not None
118+
152119
if user_input is not None:
153120
return self.async_create_entry(
154121
title=DEFAULT_TITLE,
155-
data={
156-
CONF_URL: self.server_info.base_url,
157-
},
122+
data={CONF_URL: self.url},
158123
)
124+
159125
self._set_confirm_only()
160126
return self.async_show_form(
161127
step_id="discovery_confirm",
162-
description_placeholders={"url": self.server_info.base_url},
128+
description_placeholders={"url": self.url},
163129
)

tests/components/music_assistant/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
def mock_get_server_info() -> Generator[AsyncMock]:
2424
"""Mock the function to get server info."""
2525
with patch(
26-
"homeassistant.components.music_assistant.config_flow.get_server_info"
26+
"homeassistant.components.music_assistant.config_flow._get_server_info"
2727
) as mock_get_server_info:
2828
mock_get_server_info.return_value = ServerInfoMessage.from_json(
2929
load_fixture("server_info_message.json", DOMAIN)

tests/components/music_assistant/test_config_flow.py

Lines changed: 7 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ async def test_full_flow(
6666
assert result["result"].unique_id == "1234"
6767

6868

69-
async def test_zero_conf_flow(
69+
async def test_zeroconf_flow(
7070
hass: HomeAssistant,
7171
mock_get_server_info: AsyncMock,
7272
) -> None:
@@ -90,21 +90,21 @@ async def test_zero_conf_flow(
9090
assert result["result"].unique_id == "1234"
9191

9292

93-
async def test_zero_conf_missing_server_id(
93+
async def test_zeroconf_invalid_discovery_info(
9494
hass: HomeAssistant,
9595
mock_get_server_info: AsyncMock,
9696
) -> None:
97-
"""Test zeroconf flow with missing server id."""
98-
bad_zero_conf_data = deepcopy(ZEROCONF_DATA)
99-
bad_zero_conf_data.properties.pop("server_id")
97+
"""Test zeroconf flow with invalid discovery info."""
98+
bad_zeroconf_data = deepcopy(ZEROCONF_DATA)
99+
bad_zeroconf_data.properties.pop("server_id")
100100
result = await hass.config_entries.flow.async_init(
101101
DOMAIN,
102102
context={"source": SOURCE_ZEROCONF},
103-
data=bad_zero_conf_data,
103+
data=bad_zeroconf_data,
104104
)
105105
await hass.async_block_till_done()
106106
assert result["type"] is FlowResultType.ABORT
107-
assert result["reason"] == "missing_server_id"
107+
assert result["reason"] == "invalid_discovery_info"
108108

109109

110110
async def test_duplicate_user(
@@ -324,46 +324,6 @@ async def test_zeroconf_existing_entry_working_url(
324324
assert mock_config_entry.data[CONF_URL] == "http://localhost:8095"
325325

326326

327-
async def test_zeroconf_existing_entry_broken_url(
328-
hass: HomeAssistant,
329-
mock_get_server_info: AsyncMock,
330-
mock_config_entry: MockConfigEntry,
331-
) -> None:
332-
"""Test zeroconf flow when existing entry has broken URL."""
333-
mock_config_entry.add_to_hass(hass)
334-
335-
# Create modified zeroconf data with different base_url
336-
modified_zeroconf_data = deepcopy(ZEROCONF_DATA)
337-
modified_zeroconf_data.properties["base_url"] = "http://discovered-working-url:8095"
338-
339-
# Mock server info with the discovered URL
340-
server_info = ServerInfoMessage.from_json(
341-
await async_load_fixture(hass, "server_info_message.json", DOMAIN)
342-
)
343-
server_info.base_url = "http://discovered-working-url:8095"
344-
mock_get_server_info.return_value = server_info
345-
346-
# First call (testing current URL) should fail, second call (testing discovered URL) should succeed
347-
mock_get_server_info.side_effect = [
348-
CannotConnect("cannot_connect"), # Current URL fails
349-
server_info, # Discovered URL works
350-
]
351-
352-
result = await hass.config_entries.flow.async_init(
353-
DOMAIN,
354-
context={"source": SOURCE_ZEROCONF},
355-
data=modified_zeroconf_data,
356-
)
357-
await hass.async_block_till_done()
358-
359-
# Should proceed to discovery confirm because current URL is broken
360-
assert result["type"] is FlowResultType.FORM
361-
assert result["step_id"] == "discovery_confirm"
362-
# Verify the URL was updated in the config entry
363-
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
364-
assert updated_entry.data[CONF_URL] == "http://discovered-working-url:8095"
365-
366-
367327
async def test_zeroconf_existing_entry_ignored(
368328
hass: HomeAssistant,
369329
mock_get_server_info: AsyncMock,
@@ -396,7 +356,3 @@ async def test_zeroconf_existing_entry_ignored(
396356
# Should abort because entry was ignored (respect user's choice)
397357
assert result["type"] is FlowResultType.ABORT
398358
assert result["reason"] == "already_configured"
399-
# Verify the ignored entry was not modified
400-
ignored_entry = hass.config_entries.async_get_entry(ignored_config_entry.entry_id)
401-
assert ignored_entry.data == {} # Still no URL field
402-
assert ignored_entry.source == SOURCE_IGNORE

0 commit comments

Comments
 (0)