mirror of
https://github.com/home-assistant/core.git
synced 2026-04-02 00:20:30 +01:00
Add reconfigure flow for systemnexa2 (#164361)
This commit is contained in:
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -1654,8 +1654,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/system_bridge/ @timmo001
|
||||
/homeassistant/components/systemmonitor/ @gjohansson-ST
|
||||
/tests/components/systemmonitor/ @gjohansson-ST
|
||||
/homeassistant/components/systemnexa2/ @konsulten @slangstrom
|
||||
/tests/components/systemnexa2/ @konsulten @slangstrom
|
||||
/homeassistant/components/systemnexa2/ @konsulten
|
||||
/tests/components/systemnexa2/ @konsulten
|
||||
/homeassistant/components/tado/ @erwindouna
|
||||
/tests/components/tado/ @erwindouna
|
||||
/homeassistant/components/tag/ @home-assistant/core
|
||||
|
||||
@@ -9,7 +9,12 @@ import aiohttp
|
||||
from sn2.device import Device
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_RECONFIGURE,
|
||||
SOURCE_USER,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_MODEL,
|
||||
ATTR_SW_VERSION,
|
||||
@@ -18,7 +23,6 @@ from homeassistant.const import (
|
||||
CONF_MODEL,
|
||||
CONF_NAME,
|
||||
)
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.util.network import is_ip_address
|
||||
@@ -34,18 +38,6 @@ _SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
def _is_valid_host(ip_or_hostname: str) -> bool:
|
||||
if not ip_or_hostname:
|
||||
return False
|
||||
if is_ip_address(ip_or_hostname):
|
||||
return True
|
||||
try:
|
||||
socket.gethostbyname(ip_or_hostname)
|
||||
except socket.gaierror:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class _DiscoveryInfo:
|
||||
name: str
|
||||
@@ -67,70 +59,77 @@ class SystemNexa2ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle user-initiated flow."""
|
||||
"""Handle user-initiated configuration and reconfiguration."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="user", data_schema=_SCHEMA)
|
||||
if user_input is not None:
|
||||
host = user_input[CONF_HOST]
|
||||
if not await self._async_is_valid_host(host):
|
||||
errors["base"] = "invalid_host"
|
||||
else:
|
||||
try:
|
||||
temp_dev = await Device.initiate_device(
|
||||
host=host,
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
info = await temp_dev.get_info()
|
||||
except TimeoutError, aiohttp.ClientError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
device_id = info.information.unique_id
|
||||
device_model = info.information.model
|
||||
device_version = info.information.sw_version
|
||||
supported, error = Device.is_device_supported(
|
||||
model=device_model,
|
||||
device_version=device_version,
|
||||
)
|
||||
if device_id is None or device_version is None or not supported:
|
||||
_LOGGER.error("Unsupported model: %s", error)
|
||||
return self.async_abort(
|
||||
reason="unsupported_model",
|
||||
description_placeholders={
|
||||
ATTR_MODEL: str(device_model),
|
||||
ATTR_SW_VERSION: str(device_version),
|
||||
},
|
||||
)
|
||||
|
||||
host_or_ip = user_input[CONF_HOST]
|
||||
await self.async_set_unique_id(info.information.unique_id)
|
||||
|
||||
if not _is_valid_host(host_or_ip):
|
||||
errors["base"] = "invalid_host"
|
||||
else:
|
||||
temp_dev = await Device.initiate_device(
|
||||
host=host_or_ip,
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
if self.source == SOURCE_USER:
|
||||
self._abort_if_unique_id_configured()
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_device")
|
||||
|
||||
try:
|
||||
info = await temp_dev.get_info()
|
||||
except TimeoutError, aiohttp.ClientError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data_updates={CONF_HOST: host},
|
||||
)
|
||||
self._discovered_device = _DiscoveryInfo(
|
||||
name=info.information.name,
|
||||
host=host,
|
||||
device_id=device_id,
|
||||
model=device_model,
|
||||
device_version=device_version,
|
||||
)
|
||||
return await self._async_create_device_entry()
|
||||
|
||||
if errors:
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=_SCHEMA, errors=errors
|
||||
step_id="reconfigure",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
_SCHEMA,
|
||||
user_input or self._get_reconfigure_entry().data,
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
device_id = info.information.unique_id
|
||||
device_model = info.information.model
|
||||
device_version = info.information.sw_version
|
||||
if device_id is None or device_model is None or device_version is None:
|
||||
return self.async_abort(
|
||||
reason="unsupported_model",
|
||||
description_placeholders={
|
||||
ATTR_MODEL: str(device_model),
|
||||
ATTR_SW_VERSION: str(device_version),
|
||||
},
|
||||
)
|
||||
|
||||
self._discovered_device = _DiscoveryInfo(
|
||||
name=info.information.name,
|
||||
host=host_or_ip,
|
||||
device_id=device_id,
|
||||
model=device_model,
|
||||
device_version=device_version,
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
supported, error = Device.is_device_supported(
|
||||
model=self._discovered_device.model,
|
||||
device_version=self._discovered_device.device_version,
|
||||
)
|
||||
if not supported:
|
||||
_LOGGER.error("Unsupported model: %s", error)
|
||||
raise AbortFlow(
|
||||
reason="unsupported_model",
|
||||
description_placeholders={
|
||||
ATTR_MODEL: str(self._discovered_device.model),
|
||||
ATTR_SW_VERSION: str(self._discovered_device.device_version),
|
||||
},
|
||||
)
|
||||
await self._async_set_unique_id()
|
||||
|
||||
return await self._async_create_device_entry()
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
@@ -198,3 +197,22 @@ class SystemNexa2ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_DEVICE_ID: self._discovered_device.device_id,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
async def _async_is_valid_host(self, ip_or_hostname: str) -> bool:
|
||||
|
||||
if not ip_or_hostname:
|
||||
return False
|
||||
if is_ip_address(ip_or_hostname):
|
||||
return True
|
||||
try:
|
||||
await self.hass.async_add_executor_job(socket.gethostbyname, ip_or_hostname)
|
||||
|
||||
except socket.gaierror:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"domain": "systemnexa2",
|
||||
"name": "System Nexa 2",
|
||||
"codeowners": ["@konsulten", "@slangstrom"],
|
||||
"codeowners": ["@konsulten"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/systemnexa2",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-sn2==0.4.0"],
|
||||
"zeroconf": ["_systemnexa2._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ rules:
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: No icons referenced currently
|
||||
reconfiguration-flow: todo
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: At the moment there are no repairable situations.
|
||||
|
||||
@@ -20,6 +20,15 @@
|
||||
"description": "Do you want to add the device `{name}` to Home Assistant?",
|
||||
"title": "Discovered Nexa System 2 device"
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:component::systemnexa2::config::step::user::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::systemnexa2::config::step::user::data_description::host%]"
|
||||
},
|
||||
"description": "Update the IP address or hostname if your device has been moved to a different address on the network"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "IP/hostname"
|
||||
|
||||
@@ -291,3 +291,120 @@ async def test_zeroconf_discovery_none_values(hass: HomeAssistant) -> None:
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "unsupported_model"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_system_nexa_2_device")
|
||||
async def test_reconfigure_flow(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test reconfiguration flow."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
assert mock_config_entry.data[CONF_HOST] != "10.0.0.132"
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
|
||||
# Test successful reconfiguration with new host
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "10.0.0.132"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert mock_config_entry.data[CONF_HOST] == "10.0.0.132"
|
||||
assert mock_config_entry.data[CONF_DEVICE_ID] == "aabbccddee02"
|
||||
assert mock_config_entry.data[CONF_MODEL] == "WPO-01"
|
||||
assert mock_config_entry.data[CONF_NAME] == "Outdoor Smart Plug"
|
||||
|
||||
|
||||
async def test_reconfigure_flow_invalid_host(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test reconfiguration flow with invalid host."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
|
||||
# Mock socket.gethostbyname to raise error for invalid hostname
|
||||
with patch(
|
||||
"homeassistant.components.systemnexa2.config_flow.socket.gethostbyname",
|
||||
side_effect=socket.gaierror(-2, "Name or service not known"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "invalid_hostname"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_host"}
|
||||
|
||||
|
||||
async def test_reconfigure_flow_cannot_connect(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_system_nexa_2_device: MagicMock,
|
||||
) -> None:
|
||||
"""Test reconfiguration flow with connection error."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
|
||||
mock_system_nexa_2_device.initiate_device.side_effect = TimeoutError(
|
||||
"Connection failed"
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "10.0.0.132"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
mock_system_nexa_2_device.initiate_device.side_effect = Exception("Unknown")
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "10.0.0.132"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_reconfigure_flow_wrong_device(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_system_nexa_2_device: MagicMock,
|
||||
) -> None:
|
||||
"""Test reconfiguration flow with different device."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
|
||||
different_device_info = InformationData(
|
||||
name="Different Device",
|
||||
model="Test Model",
|
||||
unique_id="different_device_id",
|
||||
sw_version="Test Model Version",
|
||||
hw_version="Test HW Version",
|
||||
wifi_dbm=-50,
|
||||
wifi_ssid="Test WiFi SSID",
|
||||
dimmable=False,
|
||||
)
|
||||
mock_system_nexa_2_device.return_value.get_info.return_value = InformationUpdate(
|
||||
information=different_device_info
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "10.0.0.132"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "wrong_device"
|
||||
|
||||
Reference in New Issue
Block a user