mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Handle name conflicts in ESPHome config flow (#142966)
This commit is contained in:
parent
5fd17d092b
commit
ae306893ff
@ -47,6 +47,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
)
|
||||
from .dashboard import async_get_or_create_dashboard_manager, async_set_dashboard_info
|
||||
from .manager import async_replace_device
|
||||
|
||||
ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key"
|
||||
ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk"
|
||||
@ -74,6 +75,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
# The ESPHome name as per its config
|
||||
self._device_name: str | None = None
|
||||
self._device_mac: str | None = None
|
||||
self._entry_with_name_conflict: ConfigEntry | None = None
|
||||
|
||||
async def _async_step_user_base(
|
||||
self, user_input: dict[str, Any] | None = None, error: str | None = None
|
||||
@ -137,7 +139,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle reauthorization flow when encryption was removed."""
|
||||
if user_input is not None:
|
||||
self._noise_psk = None
|
||||
return self._async_get_entry()
|
||||
return await self._async_get_entry_or_resolve_conflict()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_encryption_removed_confirm",
|
||||
@ -227,7 +229,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_authenticate()
|
||||
|
||||
self._password = ""
|
||||
return self._async_get_entry()
|
||||
return await self._async_get_entry_or_resolve_conflict()
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@ -354,6 +356,77 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
return self.async_abort(reason="service_received")
|
||||
|
||||
async def async_step_name_conflict(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle name conflict resolution."""
|
||||
assert self._entry_with_name_conflict is not None
|
||||
assert self._entry_with_name_conflict.unique_id is not None
|
||||
assert self.unique_id is not None
|
||||
assert self._device_name is not None
|
||||
return self.async_show_menu(
|
||||
step_id="name_conflict",
|
||||
menu_options=["name_conflict_migrate", "name_conflict_overwrite"],
|
||||
description_placeholders={
|
||||
"existing_mac": format_mac(self._entry_with_name_conflict.unique_id),
|
||||
"existing_title": self._entry_with_name_conflict.title,
|
||||
"mac": format_mac(self.unique_id),
|
||||
"name": self._device_name,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_name_conflict_migrate(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle migration of existing entry."""
|
||||
assert self._entry_with_name_conflict is not None
|
||||
assert self._entry_with_name_conflict.unique_id is not None
|
||||
assert self.unique_id is not None
|
||||
assert self._device_name is not None
|
||||
assert self._host is not None
|
||||
old_mac = format_mac(self._entry_with_name_conflict.unique_id)
|
||||
new_mac = format_mac(self.unique_id)
|
||||
entry_id = self._entry_with_name_conflict.entry_id
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self._entry_with_name_conflict,
|
||||
data={
|
||||
**self._entry_with_name_conflict.data,
|
||||
CONF_HOST: self._host,
|
||||
CONF_PORT: self._port or 6053,
|
||||
CONF_PASSWORD: self._password or "",
|
||||
CONF_NOISE_PSK: self._noise_psk or "",
|
||||
},
|
||||
)
|
||||
await async_replace_device(self.hass, entry_id, old_mac, new_mac)
|
||||
self.hass.config_entries.async_schedule_reload(entry_id)
|
||||
return self.async_abort(
|
||||
reason="name_conflict_migrated",
|
||||
description_placeholders={
|
||||
"existing_mac": old_mac,
|
||||
"mac": new_mac,
|
||||
"name": self._device_name,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_name_conflict_overwrite(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle creating a new entry by removing the old one and creating new."""
|
||||
assert self._entry_with_name_conflict is not None
|
||||
await self.hass.config_entries.async_remove(
|
||||
self._entry_with_name_conflict.entry_id
|
||||
)
|
||||
return self._async_get_entry()
|
||||
|
||||
async def _async_get_entry_or_resolve_conflict(self) -> ConfigFlowResult:
|
||||
"""Return the entry or resolve a conflict."""
|
||||
if self.source != SOURCE_REAUTH:
|
||||
for entry in self._async_current_entries(include_ignore=False):
|
||||
if entry.data.get(CONF_DEVICE_NAME) == self._device_name:
|
||||
self._entry_with_name_conflict = entry
|
||||
return await self.async_step_name_conflict()
|
||||
return self._async_get_entry()
|
||||
|
||||
@callback
|
||||
def _async_get_entry(self) -> ConfigFlowResult:
|
||||
config_data = {
|
||||
@ -407,7 +480,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
error = await self.try_login()
|
||||
if error:
|
||||
return await self.async_step_authenticate(error=error)
|
||||
return self._async_get_entry()
|
||||
return await self._async_get_entry_or_resolve_conflict()
|
||||
|
||||
errors = {}
|
||||
if error is not None:
|
||||
|
@ -9,7 +9,8 @@
|
||||
"mqtt_missing_mac": "Missing MAC address in MQTT properties.",
|
||||
"mqtt_missing_api": "Missing API port in MQTT properties.",
|
||||
"mqtt_missing_ip": "Missing IP address in MQTT properties.",
|
||||
"mqtt_missing_payload": "Missing MQTT Payload."
|
||||
"mqtt_missing_payload": "Missing MQTT Payload.",
|
||||
"name_conflict_migrated": "The configuration for `{name}` has been migrated to a new device with MAC address `{mac}` from `{existing_mac}`."
|
||||
},
|
||||
"error": {
|
||||
"resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address",
|
||||
@ -49,6 +50,14 @@
|
||||
"discovery_confirm": {
|
||||
"description": "Do you want to add the device `{name}` to Home Assistant?",
|
||||
"title": "Discovered ESPHome device"
|
||||
},
|
||||
"name_conflict": {
|
||||
"title": "Name conflict",
|
||||
"description": "**The name `{name}` is already being used by another device: {existing_title} (MAC address: `{existing_mac}`)**\n\nTo continue, please choose one of the following options:\n\n**Migrate Configuration to New Device:** If this is a replacement, migrate the existing settings to the new device (`{mac}`).\n**Overwrite the Existing Configuration:** If this is not a replacement, delete the old configuration for `{existing_mac}` and use the new device instead.",
|
||||
"menu_options": {
|
||||
"name_conflict_migrate": "Migrate configuration to new device",
|
||||
"name_conflict_overwrite": "Overwrite the existing configuration"
|
||||
}
|
||||
}
|
||||
},
|
||||
"flow_title": "{name}"
|
||||
|
@ -1622,3 +1622,96 @@ async def test_discovery_mqtt_initiation(
|
||||
|
||||
assert result["result"]
|
||||
assert result["result"].unique_id == "11:22:33:44:55:aa"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_user_flow_name_conflict_migrate(
|
||||
hass: HomeAssistant,
|
||||
mock_client,
|
||||
mock_setup_entry: None,
|
||||
) -> None:
|
||||
"""Test handle migration on name conflict."""
|
||||
existing_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_DEVICE_NAME: "test"},
|
||||
unique_id="11:22:33:44:55:cc",
|
||||
)
|
||||
existing_entry.add_to_hass(hass)
|
||||
mock_client.device_info = AsyncMock(
|
||||
return_value=DeviceInfo(
|
||||
uses_password=False,
|
||||
name="test",
|
||||
mac_address="11:22:33:44:55:AA",
|
||||
)
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"esphome",
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "name_conflict"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "name_conflict_migrate"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "name_conflict_migrated"
|
||||
|
||||
assert existing_entry.data == {
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_NOISE_PSK: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
}
|
||||
assert existing_entry.unique_id == "11:22:33:44:55:aa"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_zeroconf")
|
||||
async def test_user_flow_name_conflict_overwrite(
|
||||
hass: HomeAssistant,
|
||||
mock_client,
|
||||
mock_setup_entry: None,
|
||||
) -> None:
|
||||
"""Test handle overwrite on name conflict."""
|
||||
existing_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_DEVICE_NAME: "test"},
|
||||
unique_id="11:22:33:44:55:cc",
|
||||
)
|
||||
existing_entry.add_to_hass(hass)
|
||||
mock_client.device_info = AsyncMock(
|
||||
return_value=DeviceInfo(
|
||||
uses_password=False,
|
||||
name="test",
|
||||
mac_address="11:22:33:44:55:AA",
|
||||
)
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
"esphome",
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "name_conflict"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "name_conflict_overwrite"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "127.0.0.1",
|
||||
CONF_PORT: 6053,
|
||||
CONF_PASSWORD: "",
|
||||
CONF_NOISE_PSK: "",
|
||||
CONF_DEVICE_NAME: "test",
|
||||
}
|
||||
assert result["context"]["unique_id"] == "11:22:33:44:55:aa"
|
||||
|
Loading…
x
Reference in New Issue
Block a user