Handle router initialization, connection errors, and missing interfaces in options flow (#143475)

* Handle router initialization and connection errors in options flow

Added checks in the Keenetic NDMS2 options flow to handle cases where the integration is not initialized or there are connection errors. Relevant user feedback and abort reasons are now provided to ensure a better user experience.

* Add filtering saved/default options for interfaces before preparing an options form
This commit is contained in:
Andrey Kupreychik 2025-06-24 01:52:23 +04:00 committed by GitHub
parent c671ff3cf1
commit 6641cb3799
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 90 additions and 7 deletions

View File

@ -146,17 +146,27 @@ class KeeneticOptionsFlowHandler(OptionsFlow):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Manage the options.""" """Manage the options."""
if (
not hasattr(self.config_entry, "runtime_data")
or not self.config_entry.runtime_data
):
return self.async_abort(reason="not_initialized")
router = self.config_entry.runtime_data router = self.config_entry.runtime_data
interfaces: list[InterfaceInfo] = await self.hass.async_add_executor_job( try:
router.client.get_interfaces interfaces: list[InterfaceInfo] = await self.hass.async_add_executor_job(
) router.client.get_interfaces
)
except ConnectionException:
return self.async_abort(reason="cannot_connect")
self._interface_options = { self._interface_options = {
interface.name: (interface.description or interface.name) interface.name: (interface.description or interface.name)
for interface in interfaces for interface in interfaces
if interface.type.lower() == "bridge" if interface.type.lower() == "bridge"
} }
return await self.async_step_user() return await self.async_step_user()
async def async_step_user( async def async_step_user(
@ -182,9 +192,13 @@ class KeeneticOptionsFlowHandler(OptionsFlow):
): int, ): int,
vol.Required( vol.Required(
CONF_INTERFACES, CONF_INTERFACES,
default=self.config_entry.options.get( default=[
CONF_INTERFACES, [DEFAULT_INTERFACE] item
), for item in self.config_entry.options.get(
CONF_INTERFACES, [DEFAULT_INTERFACE]
)
if item in self._interface_options
],
): cv.multi_select(self._interface_options), ): cv.multi_select(self._interface_options),
vol.Optional( vol.Optional(
CONF_TRY_HOTSPOT, CONF_TRY_HOTSPOT,

View File

@ -36,6 +36,10 @@
"include_associated": "Use Wi-Fi AP associations data (ignored if hotspot data used)" "include_associated": "Use Wi-Fi AP associations data (ignored if hotspot data used)"
} }
} }
},
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"not_initialized": "The integration is not initialized yet. Can't display available options."
} }
} }
} }

View File

@ -6,10 +6,11 @@ from unittest.mock import Mock, patch
from ndms2_client import ConnectionException from ndms2_client import ConnectionException
from ndms2_client.client import InterfaceInfo, RouterInfo from ndms2_client.client import InterfaceInfo, RouterInfo
import pytest import pytest
import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components import keenetic_ndms2 as keenetic from homeassistant.components import keenetic_ndms2 as keenetic
from homeassistant.components.keenetic_ndms2 import const from homeassistant.components.keenetic_ndms2 import CONF_INTERFACES, const
from homeassistant.const import CONF_HOST, CONF_SOURCE from homeassistant.const import CONF_HOST, CONF_SOURCE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
@ -145,6 +146,70 @@ async def test_connection_error(hass: HomeAssistant, connect_error) -> None:
assert result["errors"] == {"base": "cannot_connect"} assert result["errors"] == {"base": "cannot_connect"}
async def test_options_not_initialized(hass: HomeAssistant) -> None:
"""Test the error when the integration is not initialized."""
entry = MockConfigEntry(domain=keenetic.DOMAIN, data=MOCK_DATA)
entry.add_to_hass(hass)
# not setting entry.runtime_data
result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "not_initialized"
async def test_options_connection_error(hass: HomeAssistant) -> None:
"""Test updating options."""
entry = MockConfigEntry(domain=keenetic.DOMAIN, data=MOCK_DATA)
entry.add_to_hass(hass)
def get_interfaces_error():
raise ConnectionException("Mocked failure")
# fake with connection error
entry.runtime_data = Mock(
client=Mock(get_interfaces=Mock(wraps=get_interfaces_error))
)
result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
async def test_options_interface_filter(hass: HomeAssistant) -> None:
"""Test the case when the default Home interface is missing on the router."""
entry = MockConfigEntry(domain=keenetic.DOMAIN, data=MOCK_DATA)
entry.add_to_hass(hass)
# fake interfaces
entry.runtime_data = Mock(
client=Mock(
get_interfaces=Mock(
return_value=[
InterfaceInfo.from_dict({"id": name, "type": "bridge"})
for name in ("not_a_home", "also_not_home")
]
)
)
)
result = await hass.config_entries.options.async_init(entry.entry_id)
assert result["type"] is FlowResultType.FORM
interfaces_schema = next(
i
for i, s in result["data_schema"].schema.items()
if i.schema == CONF_INTERFACES
)
assert isinstance(interfaces_schema, vol.Required)
assert interfaces_schema.default() == []
async def test_ssdp_works(hass: HomeAssistant, connect) -> None: async def test_ssdp_works(hass: HomeAssistant, connect) -> None:
"""Test host already configured and discovered.""" """Test host already configured and discovered."""