mirror of
https://github.com/home-assistant/core.git
synced 2026-08-28 18:24:50 -05:00
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Paulus Schoutsen <balloob@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
490 lines
16 KiB
Python
490 lines
16 KiB
Python
"""Test the Flexit config flow."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from modbus_connection import ModbusError, ModbusTcpParams
|
|
from modbus_connection.mock import MockModbusUnit
|
|
import pytest
|
|
|
|
from homeassistant.components.flexit.const import CONF_UNIT, DOMAIN, TYPE_TCP
|
|
from homeassistant.config_entries import (
|
|
SOURCE_RECONFIGURE,
|
|
SOURCE_USER,
|
|
ConfigEntryState,
|
|
)
|
|
from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_PORT, CONF_TYPE
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.data_entry_flow import FlowResultType, InvalidData
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
|
|
from tests.common import MockConfigEntry
|
|
|
|
TCP_USER_INPUT = {CONF_HOST: "1.1.1.1", CONF_PORT: 502, CONF_UNIT: 1}
|
|
TCP_ENTRY_DATA = {CONF_TYPE: TYPE_TCP, **TCP_USER_INPUT}
|
|
TCP_RECONFIGURE_INPUT = {CONF_HOST: "2.2.2.2", CONF_PORT: 502, CONF_UNIT: 1}
|
|
|
|
SERIAL_USER_INPUT = {
|
|
CONF_DEVICE: "/dev/ttyUSB0",
|
|
"baudrate": 57600,
|
|
CONF_UNIT: 1,
|
|
}
|
|
SERIAL_ENTRY_DATA = {CONF_TYPE: "serial", **SERIAL_USER_INPUT}
|
|
|
|
|
|
async def test_full_flow(
|
|
hass: HomeAssistant, mock_get_temporary_modbus_unit: MagicMock
|
|
) -> None:
|
|
"""Test the full TCP flow."""
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
assert result["type"] is FlowResultType.MENU
|
|
assert result["step_id"] == "user"
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "tcp"}
|
|
)
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "tcp"
|
|
assert result["errors"] == {}
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
TCP_USER_INPUT,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "Flexit"
|
|
assert result["data"] == TCP_ENTRY_DATA
|
|
mock_get_temporary_modbus_unit.assert_called_once_with(
|
|
hass, ModbusTcpParams(host="1.1.1.1", port=502), 1
|
|
)
|
|
|
|
|
|
async def test_tcp_host_is_normalized(hass: HomeAssistant) -> None:
|
|
"""Test TCP hosts are stored in lowercase."""
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "tcp"}
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {**TCP_USER_INPUT, CONF_HOST: "Device.Local"}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["data"][CONF_HOST] == "device.local"
|
|
|
|
|
|
async def test_maximum_unit(hass: HomeAssistant) -> None:
|
|
"""Test the maximum Modbus unit ID is accepted."""
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "tcp"}
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {**TCP_USER_INPUT, CONF_UNIT: 247}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["data"][CONF_UNIT] == 247
|
|
|
|
|
|
@pytest.mark.parametrize("unit", [0, 248])
|
|
async def test_unit_out_of_range(hass: HomeAssistant, unit: int) -> None:
|
|
"""Test unit IDs outside the Modbus address range are rejected."""
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "tcp"}
|
|
)
|
|
|
|
with pytest.raises(InvalidData):
|
|
await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {**TCP_USER_INPUT, CONF_UNIT: unit}
|
|
)
|
|
|
|
|
|
async def test_full_flow_serial(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test the full serial (RTU) flow."""
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
assert result["type"] is FlowResultType.MENU
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "serial"}
|
|
)
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "serial"
|
|
assert result["errors"] == {}
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
SERIAL_USER_INPUT,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "Flexit"
|
|
assert result["data"] == SERIAL_ENTRY_DATA
|
|
|
|
|
|
async def test_form_cannot_connect_and_retry(
|
|
hass: HomeAssistant,
|
|
mock_modbus_unit: MockModbusUnit,
|
|
) -> None:
|
|
"""Test we handle a connect error, then allow retrying successfully."""
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "tcp"}
|
|
)
|
|
|
|
mock_modbus_unit.fail_requests(ModbusError("update failed"))
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
TCP_USER_INPUT,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["errors"] == {"base": "cannot_connect"}
|
|
|
|
mock_modbus_unit.fail_requests(None)
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
TCP_USER_INPUT,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
|
|
|
|
async def test_form_cannot_read_device(
|
|
hass: HomeAssistant,
|
|
mock_modbus_unit: MockModbusUnit,
|
|
) -> None:
|
|
"""Test we handle a device that cannot be read while validating the flow."""
|
|
mock_modbus_unit.fail_requests(ModbusError("update failed"))
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "tcp"}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
TCP_USER_INPUT,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["errors"] == {"base": "cannot_connect"}
|
|
|
|
|
|
async def test_form_shared_link_conflict(
|
|
hass: HomeAssistant,
|
|
mock_get_temporary_modbus_unit: MagicMock,
|
|
) -> None:
|
|
"""Test an incompatible shared connection is a connection error."""
|
|
mock_get_temporary_modbus_unit.side_effect = HomeAssistantError
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "tcp"}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], TCP_USER_INPUT
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["errors"] == {"base": "cannot_connect"}
|
|
|
|
|
|
async def test_form_unknown_exception(
|
|
hass: HomeAssistant,
|
|
mock_get_temporary_modbus_unit: MagicMock,
|
|
) -> None:
|
|
"""Test we handle unknown exception."""
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "tcp"}
|
|
)
|
|
|
|
temporary_unit = mock_get_temporary_modbus_unit.side_effect
|
|
mock_get_temporary_modbus_unit.side_effect = Exception
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
TCP_USER_INPUT,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["errors"] == {"base": "unknown"}
|
|
|
|
mock_get_temporary_modbus_unit.side_effect = temporary_unit
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
TCP_USER_INPUT,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
|
|
|
|
async def test_reconfigure_flow(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test reconfiguration flow."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
|
|
)
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "reconfigure"
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
TCP_RECONFIGURE_INPUT,
|
|
)
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "reconfigure_successful"
|
|
assert mock_config_entry.data[CONF_HOST] == "2.2.2.2"
|
|
|
|
|
|
async def test_reconfigure_flow_serial(
|
|
hass: HomeAssistant,
|
|
mock_serial_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test reconfiguration flow for a serial connection."""
|
|
mock_serial_config_entry.add_to_hass(hass)
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={
|
|
"source": SOURCE_RECONFIGURE,
|
|
"entry_id": mock_serial_config_entry.entry_id,
|
|
},
|
|
)
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "reconfigure"
|
|
|
|
new_input = {**SERIAL_USER_INPUT, CONF_DEVICE: "/dev/ttyUSB1"}
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
new_input,
|
|
)
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "reconfigure_successful"
|
|
assert mock_serial_config_entry.data[CONF_DEVICE] == "/dev/ttyUSB1"
|
|
|
|
|
|
async def test_reconfigure_loaded_entry_releases_connection(
|
|
hass: HomeAssistant,
|
|
mock_serial_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test reconfiguration releases the active connection before validation."""
|
|
mock_serial_config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(mock_serial_config_entry.entry_id)
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={
|
|
"source": SOURCE_RECONFIGURE,
|
|
"entry_id": mock_serial_config_entry.entry_id,
|
|
},
|
|
)
|
|
|
|
async def check_connection_after_unload(*_: object) -> None:
|
|
assert mock_serial_config_entry.state is not ConfigEntryState.LOADED
|
|
|
|
with patch(
|
|
"homeassistant.components.flexit.config_flow.check_connection",
|
|
AsyncMock(side_effect=check_connection_after_unload),
|
|
):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {**SERIAL_USER_INPUT, "baudrate": 38400}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
|
|
|
|
async def test_reconfigure_loaded_entry_restored_after_error(
|
|
hass: HomeAssistant,
|
|
mock_serial_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test failed reconfiguration restores the active connection."""
|
|
mock_serial_config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(mock_serial_config_entry.entry_id)
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={
|
|
"source": SOURCE_RECONFIGURE,
|
|
"entry_id": mock_serial_config_entry.entry_id,
|
|
},
|
|
)
|
|
|
|
with patch(
|
|
"homeassistant.components.flexit.config_flow.check_connection",
|
|
AsyncMock(return_value="cannot_connect"),
|
|
):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {**SERIAL_USER_INPUT, "baudrate": 38400}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["errors"] == {"base": "cannot_connect"}
|
|
assert mock_serial_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
|
|
async def test_reconfigure_stops_when_unload_fails(
|
|
hass: HomeAssistant,
|
|
mock_serial_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test reconfiguration stops when the active entry cannot unload."""
|
|
mock_serial_config_entry.add_to_hass(hass)
|
|
assert await hass.config_entries.async_setup(mock_serial_config_entry.entry_id)
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={
|
|
"source": SOURCE_RECONFIGURE,
|
|
"entry_id": mock_serial_config_entry.entry_id,
|
|
},
|
|
)
|
|
|
|
with (
|
|
patch.object(
|
|
hass.config_entries, "async_unload", AsyncMock(return_value=False)
|
|
),
|
|
patch(
|
|
"homeassistant.components.flexit.config_flow.check_connection",
|
|
AsyncMock(),
|
|
) as check_connection,
|
|
):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {**SERIAL_USER_INPUT, "baudrate": 38400}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["errors"] == {"base": "unknown"}
|
|
check_connection.assert_not_awaited()
|
|
|
|
|
|
async def test_reconfigure_flow_errors(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
mock_modbus_unit: MockModbusUnit,
|
|
) -> None:
|
|
"""Test error handling in reconfiguration flow."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
|
|
)
|
|
assert result["type"] is FlowResultType.FORM
|
|
|
|
mock_modbus_unit.fail_requests(ModbusError("update failed"))
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
TCP_RECONFIGURE_INPUT,
|
|
)
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["errors"] == {"base": "cannot_connect"}
|
|
|
|
mock_modbus_unit.fail_requests(None)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
TCP_RECONFIGURE_INPUT,
|
|
)
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "reconfigure_successful"
|
|
|
|
|
|
async def test_reconfigure_flow_already_configured(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test reconfigure aborts if another entry already uses the given connection."""
|
|
other_entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
title="Flexit",
|
|
data={CONF_TYPE: TYPE_TCP, **TCP_RECONFIGURE_INPUT},
|
|
entry_id="flexit_002",
|
|
)
|
|
|
|
mock_config_entry.add_to_hass(hass)
|
|
other_entry.add_to_hass(hass)
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
TCP_RECONFIGURE_INPUT,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|
|
|
|
|
|
async def test_already_configured(
|
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
|
) -> None:
|
|
"""Test we handle already configured."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "tcp"}
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
TCP_USER_INPUT,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|
|
|
|
|
|
async def test_already_configured_host_case_insensitive(
|
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
|
) -> None:
|
|
"""Test hostname case differences cannot create duplicate entries."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
hass.config_entries.async_update_entry(
|
|
mock_config_entry, data={**mock_config_entry.data, CONF_HOST: "device.local"}
|
|
)
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "tcp"}
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {**TCP_USER_INPUT, CONF_HOST: "Device.Local"}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|