mirror of
https://github.com/home-assistant/core.git
synced 2026-08-20 21:31:17 +01:00
Add reconfiguration flow to Lyngdorf (#179411)
This commit is contained in:
@@ -4,6 +4,7 @@ import logging
|
||||
from typing import Any, override
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from lyngdorf.const import LyngdorfModel
|
||||
from lyngdorf.device import (
|
||||
async_find_receiver_model,
|
||||
async_get_device_serial,
|
||||
@@ -54,31 +55,26 @@ class LyngdorfFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if user_input is not None:
|
||||
self._host = user_input[CONF_HOST]
|
||||
|
||||
try:
|
||||
model = await async_find_receiver_model(self._host)
|
||||
except TimeoutError:
|
||||
model, serial = await self._async_probe(self._host)
|
||||
except TimeoutConnect:
|
||||
errors["base"] = "timeout_connect"
|
||||
except OSError:
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
errors["base"] = "unknown"
|
||||
|
||||
if not errors and not model:
|
||||
except UnsupportedModel:
|
||||
errors["base"] = "unsupported_model"
|
||||
|
||||
if not errors and model:
|
||||
except CannotDetermineId:
|
||||
errors["base"] = "cannot_determine_id"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
self._device_model = model.model_name
|
||||
self._name = model.model_name
|
||||
|
||||
serial = await async_get_device_serial(self._host)
|
||||
if not serial:
|
||||
errors["base"] = "cannot_determine_id"
|
||||
else:
|
||||
self._device_serial_number = serial.lower()
|
||||
await self.async_set_unique_id(self._device_serial_number)
|
||||
self._abort_if_unique_id_configured()
|
||||
return await self._create_entry()
|
||||
self._device_serial_number = serial
|
||||
await self.async_set_unique_id(serial)
|
||||
self._abort_if_unique_id_configured()
|
||||
return await self._create_entry()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
@@ -90,6 +86,76 @@ class LyngdorfFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def _async_probe(self, host: str) -> tuple[LyngdorfModel, str]:
|
||||
"""Return the model and serial of the device at a host."""
|
||||
try:
|
||||
model = await async_find_receiver_model(host)
|
||||
except TimeoutError as err:
|
||||
raise TimeoutConnect from err
|
||||
except OSError as err:
|
||||
raise CannotConnect from err
|
||||
if not model:
|
||||
raise UnsupportedModel
|
||||
|
||||
try:
|
||||
serial = await async_get_device_serial(host)
|
||||
except TimeoutError as err:
|
||||
raise TimeoutConnect from err
|
||||
except OSError as err:
|
||||
raise CannotConnect from err
|
||||
if not serial:
|
||||
raise CannotDetermineId
|
||||
|
||||
return model, serial.lower()
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration of an existing entry.
|
||||
|
||||
SSDP rediscovery only recovers a changed address while the device is
|
||||
still announcing somewhere Home Assistant can hear it, which a move to
|
||||
a static address or another subnet can end.
|
||||
"""
|
||||
errors: dict[str, str] = {}
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
|
||||
if user_input is not None:
|
||||
host = user_input[CONF_HOST]
|
||||
try:
|
||||
model, serial = await self._async_probe(host)
|
||||
except TimeoutConnect:
|
||||
errors["base"] = "timeout_connect"
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except UnsupportedModel:
|
||||
errors["base"] = "unsupported_model"
|
||||
except CannotDetermineId:
|
||||
errors["base"] = "cannot_determine_id"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(serial)
|
||||
self._abort_if_unique_id_mismatch()
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry,
|
||||
data_updates={
|
||||
CONF_HOST: host,
|
||||
CONF_MODEL: model.model_name,
|
||||
CONF_SERIAL_NUMBER: serial,
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema({vol.Required(CONF_HOST): cv.string}),
|
||||
reconfigure_entry.data,
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_step_ssdp(
|
||||
self, discovery_info: SsdpServiceInfo
|
||||
@@ -181,3 +247,19 @@ class LyngdorfFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
raise AbortFlow("cannot_determine_id")
|
||||
await self.async_set_unique_id(self._device_serial_number)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: self._host})
|
||||
|
||||
|
||||
class CannotConnect(Exception):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class TimeoutConnect(Exception):
|
||||
"""Error to indicate the device did not answer in time."""
|
||||
|
||||
|
||||
class UnsupportedModel(Exception):
|
||||
"""Error to indicate the device is not a model we support."""
|
||||
|
||||
|
||||
class CannotDetermineId(Exception):
|
||||
"""Error to indicate the device did not report a serial."""
|
||||
|
||||
@@ -74,7 +74,7 @@ rules:
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: No repair issues needed.
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"cannot_determine_id": "[%key:component::lyngdorf::config::error::cannot_determine_id%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"unique_id_mismatch": "The device at this address is a different Lyngdorf device from the one this entry was set up with.",
|
||||
"unsupported_model": "This Lyngdorf model is not supported"
|
||||
},
|
||||
"error": {
|
||||
@@ -19,6 +21,16 @@
|
||||
"confirm": {
|
||||
"description": "Do you want to set up **{name}**?"
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::lyngdorf::config::step::user::data_description::host%]"
|
||||
},
|
||||
"description": "Update the address Home Assistant uses to reach this device. It must be the same device; a different one will be rejected.",
|
||||
"title": "[%key:component::lyngdorf::config::step::user::title%]"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
|
||||
@@ -368,3 +368,162 @@ async def test_ssdp_discovery_connectivity_check_aborts(
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == expected_reason
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_find_receiver_model", "mock_get_device_serial")
|
||||
async def test_reconfigure(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test reconfiguring an entry updates the host."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "192.168.1.50"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert mock_config_entry.data[CONF_HOST] == "192.168.1.50"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_find_receiver_model")
|
||||
async def test_reconfigure_different_device(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_get_device_serial: AsyncMock,
|
||||
) -> None:
|
||||
"""Test an entry cannot be pointed at a different device."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_get_device_serial.return_value = "aabbccddeeff"
|
||||
original_host = mock_config_entry.data[CONF_HOST]
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "192.168.1.50"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "unique_id_mismatch"
|
||||
assert mock_config_entry.data[CONF_HOST] == original_host
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "error"),
|
||||
[
|
||||
(TimeoutError, "timeout_connect"),
|
||||
(OSError, "cannot_connect"),
|
||||
(Exception, "unknown"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_get_device_serial")
|
||||
async def test_reconfigure_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_find_receiver_model: AsyncMock,
|
||||
side_effect: type[Exception],
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test reconfigure surfaces connection errors and recovers."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_find_receiver_model.side_effect = side_effect
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "192.168.1.50"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
mock_find_receiver_model.side_effect = None
|
||||
mock_find_receiver_model.return_value = LyngdorfModel.MP_60
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: "192.168.1.50"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "error"),
|
||||
[
|
||||
(TimeoutError, "timeout_connect"),
|
||||
(OSError, "cannot_connect"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_find_receiver_model")
|
||||
async def test_user_flow_serial_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_get_device_serial: AsyncMock,
|
||||
side_effect: type[Exception],
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test a failure to read the serial is surfaced on the form."""
|
||||
mock_get_device_serial.side_effect = side_effect
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: "192.168.1.50"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
mock_get_device_serial.side_effect = None
|
||||
mock_get_device_serial.return_value = "0050c27c76b2"
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: "192.168.1.50"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "serial", "error"),
|
||||
[
|
||||
pytest.param(None, "0050c27c76b2", "unsupported_model", id="unsupported"),
|
||||
pytest.param(LyngdorfModel.MP_60, None, "cannot_determine_id", id="no_serial"),
|
||||
],
|
||||
)
|
||||
async def test_reconfigure_device_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_find_receiver_model: AsyncMock,
|
||||
mock_get_device_serial: AsyncMock,
|
||||
model: LyngdorfModel | None,
|
||||
serial: str | None,
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test reconfigure surfaces a device it cannot identify."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_find_receiver_model.return_value = model
|
||||
mock_get_device_serial.return_value = serial
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: "192.168.1.50"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
mock_find_receiver_model.return_value = LyngdorfModel.MP_60
|
||||
mock_get_device_serial.return_value = "0050c27c76b2"
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: "192.168.1.50"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
|
||||
Reference in New Issue
Block a user