Skip to content

Commit e496fb2

Browse files
mettolenjoostlek
andauthored
Add reconfigure flow to Saunum integration (home-assistant#157128)
Co-authored-by: Joostlek <[email protected]>
1 parent c2219aa commit e496fb2

File tree

5 files changed

+147
-10
lines changed

5 files changed

+147
-10
lines changed

homeassistant/components/saunum/config_flow.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pysaunum import SaunumClient, SaunumException
99
import voluptuous as vol
1010

11-
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
11+
from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
1212
from homeassistant.const import CONF_HOST
1313
from homeassistant.helpers import config_validation as cv
1414

@@ -46,14 +46,18 @@ class LeilSaunaConfigFlow(ConfigFlow, domain=DOMAIN):
4646
VERSION = 1
4747
MINOR_VERSION = 1
4848

49+
async def async_step_reconfigure(
50+
self, user_input: dict[str, Any] | None = None
51+
) -> ConfigFlowResult:
52+
"""Handle reconfiguration of the integration."""
53+
return await self.async_step_user(user_input)
54+
4955
async def async_step_user(
5056
self, user_input: dict[str, Any] | None = None
5157
) -> ConfigFlowResult:
5258
"""Handle the initial step."""
5359
errors: dict[str, str] = {}
54-
5560
if user_input is not None:
56-
# Check for duplicate configuration
5761
self._async_abort_entries_match(user_input)
5862

5963
try:
@@ -64,9 +68,14 @@ async def async_step_user(
6468
_LOGGER.exception("Unexpected exception")
6569
errors["base"] = "unknown"
6670
else:
67-
return self.async_create_entry(
68-
title="Saunum",
69-
data=user_input,
71+
if self.source == SOURCE_USER:
72+
return self.async_create_entry(
73+
title="Saunum",
74+
data=user_input,
75+
)
76+
return self.async_update_reload_and_abort(
77+
self._get_reconfigure_entry(),
78+
data_updates=user_input,
7079
)
7180

7281
return self.async_show_form(

homeassistant/components/saunum/quality_scale.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ rules:
4545
discovery:
4646
status: exempt
4747
comment: Device uses generic Espressif hardware with no unique identifying information (MAC OUI or hostname) that would distinguish it from other Espressif-based devices on the network.
48-
discovery-update-info: todo
48+
discovery-update-info:
49+
status: exempt
50+
comment: Device cannot be discovered and the Modbus TCP API does not provide MAC address or other unique network identifiers needed to update connection information.
4951
docs-data-update: done
5052
docs-examples: todo
5153
docs-known-limitations: done
@@ -62,7 +64,7 @@ rules:
6264
entity-translations: done
6365
exception-translations: done
6466
icon-translations: todo
65-
reconfiguration-flow: todo
67+
reconfiguration-flow: done
6668
repair-issues:
6769
status: exempt
6870
comment: This integration doesn't have any cases where raising an issue is needed.

homeassistant/components/saunum/strings.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
{
22
"config": {
33
"abort": {
4-
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
4+
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
5+
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
56
},
67
"error": {
78
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
89
"unknown": "[%key:common::config_flow::error::unknown%]"
910
},
1011
"step": {
12+
"reconfigure": {
13+
"data": {
14+
"host": "[%key:common::config_flow::data::ip%]"
15+
},
16+
"data_description": {
17+
"host": "[%key:component::saunum::config::step::user::data_description::host%]"
18+
},
19+
"description": "[%key:component::saunum::config::step::user::description%]"
20+
},
1121
"user": {
1222
"data": {
1323
"host": "[%key:common::config_flow::data::ip%]"

tests/components/saunum/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,13 @@ async def init_integration(
9494
await hass.async_block_till_done()
9595

9696
return mock_config_entry
97+
98+
99+
@pytest.fixture
100+
def mock_setup_entry() -> Generator[MagicMock]:
101+
"""Mock Saunum setup entry."""
102+
with patch(
103+
"homeassistant.components.saunum.async_setup_entry", autospec=True
104+
) as mock_setup_entry:
105+
mock_setup_entry.return_value = True
106+
yield mock_setup_entry

tests/components/saunum/test_config_flow.py

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
from unittest.mock import AsyncMock
6+
57
from pysaunum import SaunumConnectionError, SaunumException
68
import pytest
79

@@ -14,10 +16,11 @@
1416
from tests.common import MockConfigEntry
1517

1618
TEST_USER_INPUT = {CONF_HOST: "192.168.1.100"}
19+
TEST_RECONFIGURE_INPUT = {CONF_HOST: "192.168.1.200"}
1720

1821

1922
@pytest.mark.usefixtures("mock_saunum_client")
20-
async def test_full_flow(hass: HomeAssistant) -> None:
23+
async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
2124
"""Test full flow."""
2225
result = await hass.config_entries.flow.async_init(
2326
DOMAIN, context={"source": SOURCE_USER}
@@ -49,6 +52,7 @@ async def test_form_errors(
4952
mock_saunum_client,
5053
side_effect: Exception,
5154
error_base: str,
55+
mock_setup_entry: AsyncMock,
5256
) -> None:
5357
"""Test error handling and recovery."""
5458
mock_saunum_client.connect.side_effect = side_effect
@@ -96,3 +100,105 @@ async def test_form_duplicate(
96100

97101
assert result["type"] is FlowResultType.ABORT
98102
assert result["reason"] == "already_configured"
103+
104+
105+
@pytest.mark.usefixtures("mock_saunum_client")
106+
@pytest.mark.parametrize("user_input", [TEST_RECONFIGURE_INPUT, TEST_USER_INPUT])
107+
async def test_reconfigure_flow(
108+
hass: HomeAssistant,
109+
mock_config_entry: MockConfigEntry,
110+
user_input: dict[str, str],
111+
mock_setup_entry: AsyncMock,
112+
) -> None:
113+
"""Test reconfigure flow."""
114+
mock_config_entry.add_to_hass(hass)
115+
116+
result = await mock_config_entry.start_reconfigure_flow(hass)
117+
118+
assert result["type"] is FlowResultType.FORM
119+
assert result["step_id"] == "user"
120+
121+
result = await hass.config_entries.flow.async_configure(
122+
result["flow_id"],
123+
user_input,
124+
)
125+
126+
assert result["type"] is FlowResultType.ABORT
127+
assert result["reason"] == "reconfigure_successful"
128+
assert mock_config_entry.data == user_input
129+
130+
131+
@pytest.mark.parametrize(
132+
("side_effect", "error_base"),
133+
[
134+
(SaunumConnectionError("Connection failed"), "cannot_connect"),
135+
(SaunumException("Read error"), "cannot_connect"),
136+
(Exception("Unexpected error"), "unknown"),
137+
],
138+
)
139+
async def test_reconfigure_errors(
140+
hass: HomeAssistant,
141+
mock_config_entry: MockConfigEntry,
142+
mock_saunum_client,
143+
side_effect: Exception,
144+
error_base: str,
145+
mock_setup_entry: AsyncMock,
146+
) -> None:
147+
"""Test reconfigure flow error handling."""
148+
mock_config_entry.add_to_hass(hass)
149+
mock_saunum_client.connect.side_effect = side_effect
150+
151+
result = await mock_config_entry.start_reconfigure_flow(hass)
152+
153+
assert result["type"] is FlowResultType.FORM
154+
assert result["step_id"] == "user"
155+
156+
result = await hass.config_entries.flow.async_configure(
157+
result["flow_id"],
158+
TEST_RECONFIGURE_INPUT,
159+
)
160+
161+
assert result["type"] is FlowResultType.FORM
162+
assert result["errors"] == {"base": error_base}
163+
164+
# Test recovery - clear the error and try again
165+
mock_saunum_client.connect.side_effect = None
166+
167+
result = await hass.config_entries.flow.async_configure(
168+
result["flow_id"],
169+
TEST_RECONFIGURE_INPUT,
170+
)
171+
172+
assert result["type"] is FlowResultType.ABORT
173+
assert result["reason"] == "reconfigure_successful"
174+
assert mock_config_entry.data == TEST_RECONFIGURE_INPUT
175+
176+
177+
@pytest.mark.usefixtures("mock_saunum_client")
178+
async def test_reconfigure_to_existing_host(
179+
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_setup_entry: AsyncMock
180+
) -> None:
181+
"""Test reconfigure flow aborts when changing to a host used by another entry."""
182+
mock_config_entry.add_to_hass(hass)
183+
184+
# Create a second entry with a different host
185+
second_entry = MockConfigEntry(
186+
domain=DOMAIN,
187+
data=TEST_RECONFIGURE_INPUT,
188+
title="Saunum 2",
189+
)
190+
second_entry.add_to_hass(hass)
191+
192+
result = await mock_config_entry.start_reconfigure_flow(hass)
193+
194+
# Try to reconfigure first entry to use the same host as second entry
195+
result = await hass.config_entries.flow.async_configure(
196+
result["flow_id"],
197+
TEST_RECONFIGURE_INPUT, # Same host as second_entry
198+
)
199+
200+
assert result["type"] is FlowResultType.ABORT
201+
assert result["reason"] == "already_configured"
202+
203+
# Verify the original entry was not changed
204+
assert mock_config_entry.data == TEST_USER_INPUT

0 commit comments

Comments
 (0)