Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 49 additions & 4 deletions homeassistant/components/neopool/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
from homeassistant.core import callback

from .const import (
CONF_MODBUS_FRAMER,
CONF_UNIT_ID,
CONF_USE_AUX1,
CONF_USE_AUX2,
CONF_USE_AUX3,
Expand All @@ -40,8 +42,8 @@ async def _async_probe(user_input: dict[str, Any]) -> tuple[str | None, str | No
serial = await async_probe_serial(
user_input[CONF_HOST],
port=user_input[CONF_PORT],
unit_id=user_input["unit_id"],
framer=user_input["modbus_framer"],
unit_id=user_input[CONF_UNIT_ID],
framer=user_input[CONF_MODBUS_FRAMER],
)
except NeoPoolConnectionError, NeoPoolTimeoutError:
return None, "cannot_connect"
Expand Down Expand Up @@ -73,9 +75,9 @@ async def async_step_user(
{
vol.Required(CONF_HOST): str,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int),
vol.Optional("unit_id", default=DEFAULT_UNIT_ID): vol.Coerce(int),
vol.Optional(CONF_UNIT_ID, default=DEFAULT_UNIT_ID): vol.Coerce(int),
vol.Optional(
"modbus_framer",
CONF_MODBUS_FRAMER,
default=DEFAULT_MODBUS_FRAMER,
): vol.In(("tcp", "rtu")),
}
Expand All @@ -100,6 +102,49 @@ async def async_step_user(
errors=errors,
)

async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of an existing entry."""
entry = self._get_reconfigure_entry()
current = entry.data

data_schema = vol.Schema(
{
vol.Required(CONF_HOST, default=current[CONF_HOST]): str,
vol.Optional(
CONF_PORT, default=current.get(CONF_PORT, DEFAULT_PORT)
): vol.Coerce(int),
vol.Optional(
CONF_UNIT_ID,
default=current.get(CONF_UNIT_ID, DEFAULT_UNIT_ID),
): vol.Coerce(int),
vol.Optional(
CONF_MODBUS_FRAMER,
default=current.get(CONF_MODBUS_FRAMER, DEFAULT_MODBUS_FRAMER),
): vol.In(("tcp", "rtu")),
}
)

errors: dict[str, str] = {}
if user_input is not None:
merged = {**current, **user_input}
serial, error_key = await _async_probe(merged)
if error_key:
errors[CONF_HOST] = error_key
else:
await self.async_set_unique_id(serial)
self._abort_if_unique_id_mismatch(reason="serial_mismatch")
return self.async_update_reload_and_abort(entry, data=merged)

return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
data_schema, user_input or current
),
errors=errors,
)


class NeoPoolOptionsFlowHandler(OptionsFlowWithReload):
"""Handle options flow for NeoPool integration."""
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/components/neopool/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
DEFAULT_PORT = 502
DEFAULT_UNIT_ID = 1

# Options-flow keys.
CONF_UNIT_ID = "unit_id"
CONF_MODBUS_FRAMER = "modbus_framer"

CONF_USE_LIGHT = "use_light"
CONF_USE_COVER_SENSOR = "use_cover_sensor"
CONF_USE_AUX1 = "use_aux1"
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/neopool/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ rules:
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
reconfiguration-flow: done
repair-issues: done
stale-devices:
status: exempt
Expand Down
20 changes: 19 additions & 1 deletion homeassistant/components/neopool/strings.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"serial_mismatch": "The device at this address has a different serial number than the one originally configured."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"cannot_read_modbus": "Connected, but cannot read from the Modbus device. Check unit ID and framer settings."
},
"step": {
"reconfigure": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"modbus_framer": "[%key:component::neopool::config::step::user::data::modbus_framer%]",
"port": "[%key:common::config_flow::data::port%]",
"unit_id": "[%key:component::neopool::config::step::user::data::unit_id%]"
},
"data_description": {
"host": "[%key:component::neopool::config::step::user::data_description::host%]",
"modbus_framer": "[%key:component::neopool::config::step::user::data_description::modbus_framer%]",
"port": "[%key:component::neopool::config::step::user::data_description::port%]",
"unit_id": "[%key:component::neopool::config::step::user::data_description::unit_id%]"
},
"description": "Update the connection settings for your NeoPool controller.",
"title": "Reconfigure NeoPool Connection"
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
Expand Down
80 changes: 80 additions & 0 deletions tests/components/neopool/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,86 @@ async def test_user_flow_already_configured(
assert result["reason"] == "already_configured"


@pytest.mark.usefixtures("mock_neopool_client")
async def test_reconfigure_flow(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
) -> None:
"""A reconfigure flow updates the connection settings and reloads the entry."""
mock_config_entry.add_to_hass(hass)

result = await mock_config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"

result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{**USER_INPUT, CONF_HOST: "192.0.2.50", CONF_PORT: 1502},
)

assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
assert mock_config_entry.data[CONF_HOST] == "192.0.2.50"
assert mock_config_entry.data[CONF_PORT] == 1502


@pytest.mark.parametrize(
("exc_cls", "error_key"),
[
(NeoPoolConnectionError, "cannot_connect"),
(NeoPoolTimeoutError, "cannot_connect"),
(NeoPoolModbusError, "cannot_read_modbus"),
],
)
async def test_reconfigure_flow_probe_errors_recover(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
mock_socket_connection: AsyncMock,
exc_cls: type[Exception],
error_key: str,
) -> None:
"""Probe errors surface as form errors, and the flow recovers on retry."""
mock_config_entry.add_to_hass(hass)
result = await mock_config_entry.start_reconfigure_flow(hass)

mock_socket_connection.side_effect = exc_cls("boom")
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{**USER_INPUT, CONF_HOST: "192.0.2.50"},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {CONF_HOST: error_key}

mock_socket_connection.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{**USER_INPUT, CONF_HOST: "192.0.2.50"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"


async def test_reconfigure_flow_serial_mismatch(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
mock_socket_connection: AsyncMock,
) -> None:
"""A reconfigure targeting a different physical controller is rejected."""
mock_config_entry.add_to_hass(hass)
result = await mock_config_entry.start_reconfigure_flow(hass)

mock_socket_connection.return_value = "9999999999"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{**USER_INPUT, CONF_HOST: "192.0.2.50"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "serial_mismatch"


@pytest.mark.usefixtures("mock_neopool_client")
async def test_options_flow_show_form(
hass: HomeAssistant,
Expand Down
Loading