Skip to content

Commit 416f6b9

Browse files
authored
Add reconfigure flow to airOS (home-assistant#154447)
1 parent d2af875 commit 416f6b9

File tree

3 files changed

+231
-3
lines changed

3 files changed

+231
-3
lines changed

homeassistant/components/airos/config_flow.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
)
1616
import voluptuous as vol
1717

18-
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
18+
from homeassistant.config_entries import (
19+
SOURCE_REAUTH,
20+
SOURCE_RECONFIGURE,
21+
ConfigFlow,
22+
ConfigFlowResult,
23+
)
1924
from homeassistant.const import (
2025
CONF_HOST,
2126
CONF_PASSWORD,
@@ -119,7 +124,7 @@ async def _validate_and_get_device_info(
119124
else:
120125
await self.async_set_unique_id(airos_data.derived.mac)
121126

122-
if self.source == SOURCE_REAUTH:
127+
if self.source in [SOURCE_REAUTH, SOURCE_RECONFIGURE]:
123128
self._abort_if_unique_id_mismatch()
124129
else:
125130
self._abort_if_unique_id_configured()
@@ -164,3 +169,54 @@ async def async_step_reauth_confirm(
164169
),
165170
errors=self.errors,
166171
)
172+
173+
async def async_step_reconfigure(
174+
self,
175+
user_input: Mapping[str, Any] | None = None,
176+
) -> ConfigFlowResult:
177+
"""Handle reconfiguration of airOS."""
178+
self.errors = {}
179+
entry = self._get_reconfigure_entry()
180+
current_data = entry.data
181+
182+
if user_input is not None:
183+
validate_data = {**current_data, **user_input}
184+
if await self._validate_and_get_device_info(config_data=validate_data):
185+
return self.async_update_reload_and_abort(
186+
entry,
187+
data_updates=validate_data,
188+
)
189+
190+
return self.async_show_form(
191+
step_id="reconfigure",
192+
data_schema=vol.Schema(
193+
{
194+
vol.Required(CONF_PASSWORD): TextSelector(
195+
TextSelectorConfig(
196+
type=TextSelectorType.PASSWORD,
197+
autocomplete="current-password",
198+
)
199+
),
200+
vol.Required(SECTION_ADVANCED_SETTINGS): section(
201+
vol.Schema(
202+
{
203+
vol.Required(
204+
CONF_SSL,
205+
default=current_data[SECTION_ADVANCED_SETTINGS][
206+
CONF_SSL
207+
],
208+
): bool,
209+
vol.Required(
210+
CONF_VERIFY_SSL,
211+
default=current_data[SECTION_ADVANCED_SETTINGS][
212+
CONF_VERIFY_SSL
213+
],
214+
): bool,
215+
}
216+
),
217+
{"collapsed": True},
218+
),
219+
}
220+
),
221+
errors=self.errors,
222+
)

homeassistant/components/airos/strings.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,27 @@
1010
"password": "[%key:component::airos::config::step::user::data_description::password%]"
1111
}
1212
},
13+
"reconfigure": {
14+
"data": {
15+
"password": "[%key:common::config_flow::data::password%]"
16+
},
17+
"data_description": {
18+
"password": "[%key:component::airos::config::step::user::data_description::password%]"
19+
},
20+
"sections": {
21+
"advanced_settings": {
22+
"name": "[%key:component::airos::config::step::user::sections::advanced_settings::name%]",
23+
"data": {
24+
"ssl": "[%key:component::airos::config::step::user::sections::advanced_settings::data::ssl%]",
25+
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
26+
},
27+
"data_description": {
28+
"ssl": "[%key:component::airos::config::step::user::sections::advanced_settings::data_description::ssl%]",
29+
"verify_ssl": "[%key:component::airos::config::step::user::sections::advanced_settings::data_description::verify_ssl%]"
30+
}
31+
}
32+
}
33+
},
1334
"user": {
1435
"data": {
1536
"host": "[%key:common::config_flow::data::host%]",
@@ -23,6 +44,7 @@
2344
},
2445
"sections": {
2546
"advanced_settings": {
47+
"name": "Advanced settings",
2648
"data": {
2749
"ssl": "Use HTTPS",
2850
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
@@ -44,6 +66,7 @@
4466
"abort": {
4567
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
4668
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
69+
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
4770
"unique_id_mismatch": "Re-authentication should be used for the same device not a new one"
4871
}
4972
},

tests/components/airos/test_config_flow.py

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import pytest
1212

1313
from homeassistant.components.airos.const import DOMAIN, SECTION_ADVANCED_SETTINGS
14-
from homeassistant.config_entries import SOURCE_USER
14+
from homeassistant.config_entries import SOURCE_RECONFIGURE, SOURCE_USER
1515
from homeassistant.const import (
1616
CONF_HOST,
1717
CONF_PASSWORD,
@@ -26,6 +26,7 @@
2626

2727
NEW_PASSWORD = "new_password"
2828
REAUTH_STEP = "reauth_confirm"
29+
RECONFIGURE_STEP = "reconfigure"
2930

3031
MOCK_CONFIG = {
3132
CONF_HOST: "1.1.1.1",
@@ -253,3 +254,151 @@ async def test_reauth_unique_id_mismatch(
253254

254255
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
255256
assert updated_entry.data[CONF_PASSWORD] != NEW_PASSWORD
257+
258+
259+
async def test_successful_reconfigure(
260+
hass: HomeAssistant,
261+
mock_airos_client: AsyncMock,
262+
mock_config_entry: MockConfigEntry,
263+
) -> None:
264+
"""Test successful reconfigure."""
265+
mock_config_entry.add_to_hass(hass)
266+
267+
await hass.config_entries.async_setup(mock_config_entry.entry_id)
268+
269+
result = await hass.config_entries.flow.async_init(
270+
DOMAIN,
271+
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
272+
)
273+
274+
assert result["type"] is FlowResultType.FORM
275+
assert result["step_id"] == RECONFIGURE_STEP
276+
277+
user_input = {
278+
CONF_PASSWORD: NEW_PASSWORD,
279+
SECTION_ADVANCED_SETTINGS: {
280+
CONF_SSL: True,
281+
CONF_VERIFY_SSL: True,
282+
},
283+
}
284+
285+
result = await hass.config_entries.flow.async_configure(
286+
result["flow_id"],
287+
user_input=user_input,
288+
)
289+
290+
assert result["type"] is FlowResultType.ABORT
291+
assert result["reason"] == "reconfigure_successful"
292+
293+
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
294+
assert updated_entry.data[CONF_PASSWORD] == NEW_PASSWORD
295+
assert updated_entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL] is True
296+
assert updated_entry.data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL] is True
297+
298+
assert updated_entry.data[CONF_HOST] == MOCK_CONFIG[CONF_HOST]
299+
assert updated_entry.data[CONF_USERNAME] == MOCK_CONFIG[CONF_USERNAME]
300+
301+
302+
@pytest.mark.parametrize(
303+
("reconfigure_exception", "expected_error"),
304+
[
305+
(AirOSConnectionAuthenticationError, "invalid_auth"),
306+
(AirOSDeviceConnectionError, "cannot_connect"),
307+
(AirOSKeyDataMissingError, "key_data_missing"),
308+
(Exception, "unknown"),
309+
],
310+
ids=[
311+
"invalid_auth",
312+
"cannot_connect",
313+
"key_data_missing",
314+
"unknown",
315+
],
316+
)
317+
async def test_reconfigure_flow_failure(
318+
hass: HomeAssistant,
319+
mock_airos_client: AsyncMock,
320+
mock_config_entry: MockConfigEntry,
321+
reconfigure_exception: Exception,
322+
expected_error: str,
323+
) -> None:
324+
"""Test reconfigure from start (failure) to finish (success)."""
325+
mock_config_entry.add_to_hass(hass)
326+
327+
await hass.config_entries.async_setup(mock_config_entry.entry_id)
328+
329+
result = await hass.config_entries.flow.async_init(
330+
DOMAIN,
331+
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
332+
)
333+
334+
user_input = {
335+
CONF_PASSWORD: NEW_PASSWORD,
336+
SECTION_ADVANCED_SETTINGS: {
337+
CONF_SSL: True,
338+
CONF_VERIFY_SSL: True,
339+
},
340+
}
341+
342+
mock_airos_client.login.side_effect = reconfigure_exception
343+
344+
result = await hass.config_entries.flow.async_configure(
345+
result["flow_id"],
346+
user_input=user_input,
347+
)
348+
349+
assert result["type"] is FlowResultType.FORM
350+
assert result["step_id"] == RECONFIGURE_STEP
351+
assert result["errors"] == {"base": expected_error}
352+
353+
mock_airos_client.login.side_effect = None
354+
result = await hass.config_entries.flow.async_configure(
355+
result["flow_id"],
356+
user_input=user_input,
357+
)
358+
359+
assert result["type"] is FlowResultType.ABORT
360+
assert result["reason"] == "reconfigure_successful"
361+
362+
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
363+
assert updated_entry.data[CONF_PASSWORD] == NEW_PASSWORD
364+
365+
366+
async def test_reconfigure_unique_id_mismatch(
367+
hass: HomeAssistant,
368+
mock_airos_client: AsyncMock,
369+
mock_config_entry: MockConfigEntry,
370+
) -> None:
371+
"""Test reconfiguration failure when the unique ID changes."""
372+
mock_config_entry.add_to_hass(hass)
373+
await hass.config_entries.async_setup(mock_config_entry.entry_id)
374+
375+
result = await hass.config_entries.flow.async_init(
376+
DOMAIN,
377+
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
378+
)
379+
flow_id = result["flow_id"]
380+
381+
mock_airos_client.status.return_value.derived.mac = "FF:23:45:67:89:AB"
382+
383+
user_input = {
384+
CONF_PASSWORD: NEW_PASSWORD,
385+
SECTION_ADVANCED_SETTINGS: {
386+
CONF_SSL: True,
387+
CONF_VERIFY_SSL: True,
388+
},
389+
}
390+
391+
result = await hass.config_entries.flow.async_configure(
392+
flow_id,
393+
user_input=user_input,
394+
)
395+
396+
assert result["type"] is FlowResultType.ABORT
397+
assert result["reason"] == "unique_id_mismatch"
398+
399+
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
400+
assert updated_entry.data[CONF_PASSWORD] == MOCK_CONFIG[CONF_PASSWORD]
401+
assert (
402+
updated_entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL]
403+
== MOCK_CONFIG[SECTION_ADVANCED_SETTINGS][CONF_SSL]
404+
)

0 commit comments

Comments
 (0)