Skip to content
Draft
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
44 changes: 43 additions & 1 deletion homeassistant/components/iaqualink/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@
)
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import (
SOURCE_DHCP,
SOURCE_REAUTH,
ConfigFlow,
ConfigFlowResult,
)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.util.ssl import SSL_ALPN_HTTP11_HTTP2

from .const import DOMAIN
Expand All @@ -33,6 +39,16 @@ class AqualinkFlowHandler(ConfigFlow, domain=DOMAIN):

VERSION = 1

def __init__(self) -> None:
"""Initialize the config flow."""
self._discovered_hostname: str | None = None
self._discovered_ip: str | None = None

async def _async_set_single_instance_unique_id(self) -> None:
"""Assign the unique ID used by this single-instance integration."""
await self.async_set_unique_id(DOMAIN, raise_on_progress=False)
self._abort_if_unique_id_configured(error="single_instance_allowed")
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the missing single_instance_allowed abort translation key so the new abort reason can be shown to users.

_abort_if_unique_id_configured(error="single_instance_allowed") requires a corresponding entry in homeassistant/components/iaqualink/strings.json (typically mapping to [%key:common::config_flow::abort::single_instance_allowed%]), otherwise the abort reason will be untranslated / fail translation validation.

Suggested change
self._abort_if_unique_id_configured(error="single_instance_allowed")
self._abort_if_unique_id_configured()

Copilot uses AI. Check for mistakes.

async def _async_test_credentials(
self, user_input: dict[str, Any]
) -> dict[str, str]:
Expand All @@ -53,10 +69,24 @@ async def _async_test_credentials(

return {}

async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle a DHCP discovery."""
await self._async_set_single_instance_unique_id()

self._discovered_hostname = discovery_info.hostname
self._discovered_ip = discovery_info.ip

return await self.async_step_user()

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow start."""
if self.source not in (SOURCE_DHCP, SOURCE_REAUTH):
await self._async_set_single_instance_unique_id()

errors = {}

if user_input is not None:
Expand All @@ -66,9 +96,21 @@ async def async_step_user(
title=user_input[CONF_USERNAME], data=user_input
)

discovery = ""
if (
self.source == SOURCE_DHCP
and self._discovered_hostname is not None
and self._discovered_ip is not None
):
discovery = (
"A likely iAquaLink device was discovered on your network at "
f"{self._discovered_ip} ({self._discovered_hostname}). "
)

return self.async_show_form(
step_id="user",
data_schema=CREDENTIALS_DATA_SCHEMA,
description_placeholders={"discovery": discovery},
Comment on lines +99 to +113
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make the DHCP discovery message translatable by moving the user-facing sentence into strings.json and only passing raw values as placeholders.

Right now the discovery text is hard-coded in English in the config flow and injected via {discovery}. Consider adding a dedicated discovery/confirm step (with description placeholders like {ip} and {hostname}) or a separate translated description key, so translations can render the message properly.

Suggested change
discovery = ""
if (
self.source == SOURCE_DHCP
and self._discovered_hostname is not None
and self._discovered_ip is not None
):
discovery = (
"A likely iAquaLink device was discovered on your network at "
f"{self._discovered_ip} ({self._discovered_hostname}). "
)
return self.async_show_form(
step_id="user",
data_schema=CREDENTIALS_DATA_SCHEMA,
description_placeholders={"discovery": discovery},
description_placeholders: dict[str, str] | None = None
if (
self.source == SOURCE_DHCP
and self._discovered_hostname is not None
and self._discovered_ip is not None
):
description_placeholders = {
"ip": self._discovered_ip,
"hostname": self._discovered_hostname,
}
return self.async_show_form(
step_id="user",
data_schema=CREDENTIALS_DATA_SCHEMA,
description_placeholders=description_placeholders,

Copilot uses AI. Check for mistakes.
errors=errors,
)

Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/iaqualink/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"name": "Jandy iAqualink",
"codeowners": ["@flz"],
"config_flow": true,
"dhcp": [{ "hostname": "iaqualink-*" }],
Comment thread
flz marked this conversation as resolved.
"documentation": "https://www.home-assistant.io/integrations/iaqualink",
"integration_type": "hub",
"iot_class": "cloud_polling",
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/iaqualink/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"description": "Please enter the username and password for your iAqualink account.",
"description": "{discovery}Please enter the username and password for your iAqualink account.",
"title": "Connect to iAqualink"
}
}
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/generated/dhcp.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 64 additions & 1 deletion tests/components/iaqualink/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@
)

from homeassistant.components.iaqualink import DOMAIN, config_flow
from homeassistant.config_entries import SOURCE_USER
from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo

from tests.common import MockConfigEntry

DHCP_DISCOVERY = DhcpServiceInfo(
ip="192.168.1.23",
hostname="iAquaLink-123456",
macaddress="001122334455",
)


async def test_already_configured(
hass: HomeAssistant,
Expand Down Expand Up @@ -45,12 +52,32 @@ async def test_without_config(hass: HomeAssistant) -> None:
assert result["errors"] == {}


async def test_dhcp_discovery_starts_user_flow(hass: HomeAssistant) -> None:
"""Test DHCP discovery starts the user flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_DHCP},
data=DHCP_DISCOVERY,
)

assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
assert result["description_placeholders"] == {
"discovery": (
"A likely iAquaLink device was discovered on your network at "
"192.168.1.23 (iAquaLink-123456). "
)
}


async def test_with_invalid_credentials(
hass: HomeAssistant, config_data: dict[str, str]
) -> None:
"""Test config flow with invalid username and/or password."""
flow = config_flow.AqualinkFlowHandler()
flow.hass = hass
flow.context = {}

with patch(
"homeassistant.components.iaqualink.config_flow.AqualinkClient.login",
Expand All @@ -69,6 +96,7 @@ async def test_service_exception(
"""Test config flow encountering service exception."""
flow = config_flow.AqualinkFlowHandler()
flow.hass = hass
flow.context = {}

with patch(
"homeassistant.components.iaqualink.config_flow.AqualinkClient.login",
Expand Down Expand Up @@ -100,6 +128,41 @@ async def test_with_existing_config(
assert result["data"] == config_data


async def test_user_flow_sets_domain_unique_id(
hass: HomeAssistant, config_data: dict[str, str]
) -> None:
"""Test the user flow stores the single-instance unique ID."""
flow = config_flow.AqualinkFlowHandler()
flow.hass = hass
flow.context = {}

with patch(
"homeassistant.components.iaqualink.config_flow.AqualinkClient.login",
return_value=None,
):
result = await flow.async_step_user(config_data)

assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["context"]["unique_id"] == DOMAIN


async def test_dhcp_discovery_aborts_if_already_configured(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test DHCP discovery aborts if iaqualink is already configured."""
config_entry.add_to_hass(hass)

result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_DHCP},
data=DHCP_DISCOVERY,
)

assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "single_instance_allowed"


async def test_reauth_success(hass: HomeAssistant, config_data: dict[str, str]) -> None:
"""Test successful reauthentication."""
entry = MockConfigEntry(
Expand Down
Loading