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
11 changes: 7 additions & 4 deletions homeassistant/components/sleepiq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
SleepIQPauseUpdateCoordinator,
SleepIQSleepDataCoordinator,
)
from .helpers import is_invalid_auth

_LOGGER = logging.getLogger(__name__)

Expand All @@ -51,8 +52,6 @@
},
extra=vol.ALLOW_EXTRA,
)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up sleepiq component."""
if DOMAIN in config:
Expand All @@ -78,8 +77,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: SleepIQConfigEntry) -> b
try:
await gateway.login(email, password)
except SleepIQLoginException as err:
_LOGGER.error("Could not authenticate with SleepIQ server")
raise ConfigEntryAuthFailed(err) from err
if is_invalid_auth(err):
_LOGGER.error("Could not authenticate with SleepIQ server")
raise ConfigEntryAuthFailed(err) from err
raise ConfigEntryNotReady(
str(err) or "Retryable SleepIQ login failure"
) from err
except SleepIQTimeoutException as err:
raise ConfigEntryNotReady(
str(err) or "Timed out during authentication"
Expand Down
7 changes: 5 additions & 2 deletions homeassistant/components/sleepiq/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN
from .helpers import is_invalid_auth

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -117,8 +118,10 @@ async def try_connection(hass: HomeAssistant, user_input: dict[str, Any]) -> str
gateway = AsyncSleepIQ(client_session=client_session)
try:
await gateway.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])
except SleepIQLoginException:
return "invalid_auth"
except SleepIQLoginException as err:
if is_invalid_auth(err):
return "invalid_auth"
return "cannot_connect"
except SleepIQTimeoutException:
Comment on lines 120 to 125
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

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

Add regression coverage for the reauth flow’s failure cases since try_connection now maps SleepIQLoginException to either invalid_auth or cannot_connect and reauth uses the same helper.

Copilot uses AI. Check for mistakes.
return "cannot_connect"

Expand Down
12 changes: 12 additions & 0 deletions homeassistant/components/sleepiq/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Helpers for the SleepIQ integration."""

from __future__ import annotations

from asyncsleepiq import SleepIQLoginException

INVALID_AUTH_ERROR = "incorrect username or password"


def is_invalid_auth(err: SleepIQLoginException) -> bool:
"""Return if a SleepIQ login exception indicates invalid credentials."""
return INVALID_AUTH_ERROR in str(err).lower()
44 changes: 43 additions & 1 deletion tests/components/sleepiq/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,14 @@ async def test_show_set_form(hass: HomeAssistant) -> None:
@pytest.mark.parametrize(
("side_effect", "error"),
[
(SleepIQLoginException, "invalid_auth"),
(
SleepIQLoginException("Incorrect username or password"),
"invalid_auth",
),
(
SleepIQLoginException("Connection failure: Cannot connect to host"),
"cannot_connect",
),
(SleepIQTimeoutException, "cannot_connect"),
],
)
Expand Down Expand Up @@ -115,3 +122,38 @@ async def test_reauth_password(hass: HomeAssistant) -> None:

assert result2["type"] is FlowResultType.ABORT
assert result2["reason"] == "reauth_successful"


@pytest.mark.parametrize(
("side_effect", "error"),
[
(
SleepIQLoginException("Incorrect username or password"),
"invalid_auth",
),
(
SleepIQLoginException("Connection failure: Cannot connect to host"),
"cannot_connect",
),
(SleepIQTimeoutException, "cannot_connect"),
],
Comment on lines +127 to +139
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

Reduce duplicated parametrization data by extracting the shared (side_effect, error) cases into a single constant/fixture and reusing it in both test_login_failure and test_reauth_failure to keep future updates in sync.

Copilot uses AI. Check for mistakes.
)
async def test_reauth_failure(hass: HomeAssistant, side_effect, error) -> None:
"""Test reauth form errors on login failure."""

entry = await setup_platform(hass)
result = await entry.start_reauth_flow(hass)

with patch(
"homeassistant.components.sleepiq.config_flow.AsyncSleepIQ.login",
side_effect=side_effect,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"password": "password"},
)
await hass.async_block_till_done()

assert result2["type"] is FlowResultType.FORM
assert result2["step_id"] == "reauth_confirm"
assert result2["errors"] == {"base": error}
18 changes: 17 additions & 1 deletion tests/components/sleepiq/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,24 @@ async def test_unload_entry(hass: HomeAssistant, mock_asyncsleepiq) -> None:

async def test_entry_setup_login_error(hass: HomeAssistant, mock_asyncsleepiq) -> None:
"""Test when sleepiq client is unable to login."""
mock_asyncsleepiq.login.side_effect = SleepIQLoginException
mock_asyncsleepiq.login.side_effect = SleepIQLoginException(
"Incorrect username or password"
)
entry = await setup_platform(hass, None)
assert not await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.SETUP_ERROR


async def test_entry_setup_retryable_login_error(
hass: HomeAssistant, mock_asyncsleepiq
) -> None:
"""Test when sleepiq client cannot connect during login."""
mock_asyncsleepiq.login.side_effect = SleepIQLoginException(
"Connection failure: Cannot connect to host"
)
entry = await setup_platform(hass, None)
assert not await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.SETUP_RETRY


async def test_entry_setup_timeout_error(
Expand All @@ -67,6 +82,7 @@ async def test_entry_setup_timeout_error(
mock_asyncsleepiq.login.side_effect = SleepIQTimeoutException
entry = await setup_platform(hass, None)
assert not await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.SETUP_RETRY


async def test_update_interval(hass: HomeAssistant, mock_asyncsleepiq) -> None:
Expand Down
Loading