Skip to content

Commit d835025

Browse files
MartinHjelmarefrenck
authored andcommitted
Fix Thread flow abort on multiple flows (home-assistant#153048)
1 parent 08e81b2 commit d835025

File tree

4 files changed

+93
-12
lines changed

4 files changed

+93
-12
lines changed

homeassistant/components/thread/config_flow.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
from typing import Any
66

77
from homeassistant.components import onboarding
8-
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
8+
from homeassistant.config_entries import (
9+
DEFAULT_DISCOVERY_UNIQUE_ID,
10+
ConfigFlow,
11+
ConfigFlowResult,
12+
)
913
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
1014

1115
from .const import DOMAIN
@@ -18,14 +22,18 @@ class ThreadConfigFlow(ConfigFlow, domain=DOMAIN):
1822

1923
async def async_step_import(self, import_data: None) -> ConfigFlowResult:
2024
"""Set up by import from async_setup."""
21-
await self._async_handle_discovery_without_unique_id()
25+
await self.async_set_unique_id(
26+
DEFAULT_DISCOVERY_UNIQUE_ID, raise_on_progress=False
27+
)
2228
return self.async_create_entry(title="Thread", data={})
2329

2430
async def async_step_user(
2531
self, user_input: dict[str, str] | None = None
2632
) -> ConfigFlowResult:
2733
"""Set up by import from async_setup."""
28-
await self._async_handle_discovery_without_unique_id()
34+
await self.async_set_unique_id(
35+
DEFAULT_DISCOVERY_UNIQUE_ID, raise_on_progress=False
36+
)
2937
return self.async_create_entry(title="Thread", data={})
3038

3139
async def async_step_zeroconf(

homeassistant/components/thread/manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
"integration_type": "service",
99
"iot_class": "local_polling",
1010
"requirements": ["python-otbr-api==2.7.0", "pyroute2==0.7.5"],
11+
"single_config_entry": true,
1112
"zeroconf": ["_meshcop._udp.local."]
1213
}

homeassistant/generated/integrations.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6807,7 +6807,8 @@
68076807
"name": "Thread",
68086808
"integration_type": "service",
68096809
"config_flow": true,
6810-
"iot_class": "local_polling"
6810+
"iot_class": "local_polling",
6811+
"single_config_entry": true
68116812
},
68126813
"tibber": {
68136814
"name": "Tibber",

tests/components/thread/test_config_flow.py

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from ipaddress import ip_address
44
from unittest.mock import patch
55

6+
import pytest
7+
68
from homeassistant.components import thread
79
from homeassistant.core import HomeAssistant
810
from homeassistant.data_entry_flow import FlowResultType
@@ -56,14 +58,18 @@ async def test_import(hass: HomeAssistant) -> None:
5658
assert config_entry.unique_id is None
5759

5860

59-
async def test_import_then_zeroconf(hass: HomeAssistant) -> None:
60-
"""Test the import flow."""
61+
@pytest.mark.parametrize("source", ["import", "user"])
62+
async def test_single_instance_allowed_zeroconf(
63+
hass: HomeAssistant,
64+
source: str,
65+
) -> None:
66+
"""Test zeroconf single instance allowed abort reason."""
6167
with patch(
6268
"homeassistant.components.thread.async_setup_entry",
6369
return_value=True,
6470
) as mock_setup_entry:
6571
result = await hass.config_entries.flow.async_init(
66-
thread.DOMAIN, context={"source": "import"}
72+
thread.DOMAIN, context={"source": source}
6773
)
6874

6975
assert result["type"] is FlowResultType.CREATE_ENTRY
@@ -77,7 +83,7 @@ async def test_import_then_zeroconf(hass: HomeAssistant) -> None:
7783
)
7884

7985
assert result["type"] is FlowResultType.ABORT
80-
assert result["reason"] == "already_configured"
86+
assert result["reason"] == "single_instance_allowed"
8187
assert len(mock_setup_entry.mock_calls) == 0
8288

8389

@@ -152,8 +158,45 @@ async def test_zeroconf_setup_onboarding(hass: HomeAssistant) -> None:
152158
assert len(mock_setup_entry.mock_calls) == 1
153159

154160

155-
async def test_zeroconf_then_import(hass: HomeAssistant) -> None:
156-
"""Test the import flow."""
161+
@pytest.mark.parametrize(
162+
("first_source", "second_source"), [("import", "user"), ("user", "import")]
163+
)
164+
async def test_import_and_user(
165+
hass: HomeAssistant,
166+
first_source: str,
167+
second_source: str,
168+
) -> None:
169+
"""Test single instance allowed for user and import."""
170+
with patch(
171+
"homeassistant.components.thread.async_setup_entry",
172+
return_value=True,
173+
) as mock_setup_entry:
174+
result = await hass.config_entries.flow.async_init(
175+
thread.DOMAIN, context={"source": first_source}
176+
)
177+
178+
assert result["type"] is FlowResultType.CREATE_ENTRY
179+
assert len(mock_setup_entry.mock_calls) == 1
180+
181+
with patch(
182+
"homeassistant.components.thread.async_setup_entry",
183+
return_value=True,
184+
) as mock_setup_entry:
185+
result = await hass.config_entries.flow.async_init(
186+
thread.DOMAIN, context={"source": second_source}
187+
)
188+
189+
assert result["type"] is FlowResultType.ABORT
190+
assert result["reason"] == "single_instance_allowed"
191+
assert len(mock_setup_entry.mock_calls) == 0
192+
193+
194+
@pytest.mark.parametrize("source", ["import", "user"])
195+
async def test_zeroconf_then_import_user(
196+
hass: HomeAssistant,
197+
source: str,
198+
) -> None:
199+
"""Test single instance allowed abort reason for import/user flow."""
157200
result = await hass.config_entries.flow.async_init(
158201
thread.DOMAIN, context={"source": "zeroconf"}, data=TEST_ZEROCONF_RECORD
159202
)
@@ -169,9 +212,37 @@ async def test_zeroconf_then_import(hass: HomeAssistant) -> None:
169212
return_value=True,
170213
) as mock_setup_entry:
171214
result = await hass.config_entries.flow.async_init(
172-
thread.DOMAIN, context={"source": "import"}
215+
thread.DOMAIN, context={"source": source}
173216
)
174217

175218
assert result["type"] is FlowResultType.ABORT
176-
assert result["reason"] == "already_configured"
219+
assert result["reason"] == "single_instance_allowed"
177220
assert len(mock_setup_entry.mock_calls) == 0
221+
222+
223+
@pytest.mark.parametrize("source", ["import", "user"])
224+
async def test_zeroconf_in_progress_then_import_user(
225+
hass: HomeAssistant,
226+
source: str,
227+
) -> None:
228+
"""Test priority (import/user) flow with zeroconf flow in progress."""
229+
result = await hass.config_entries.flow.async_init(
230+
thread.DOMAIN, context={"source": "zeroconf"}, data=TEST_ZEROCONF_RECORD
231+
)
232+
233+
assert result["type"] is FlowResultType.FORM
234+
assert result["step_id"] == "confirm"
235+
236+
with patch(
237+
"homeassistant.components.thread.async_setup_entry",
238+
return_value=True,
239+
) as mock_setup_entry:
240+
result = await hass.config_entries.flow.async_init(
241+
thread.DOMAIN, context={"source": source}
242+
)
243+
244+
assert result["type"] is FlowResultType.CREATE_ENTRY
245+
assert mock_setup_entry.call_count == 1
246+
247+
flows_in_progress = hass.config_entries.flow.async_progress()
248+
assert len(flows_in_progress) == 0

0 commit comments

Comments
 (0)