Improve ESPHome abort messages for already-configured devices (#143289)

* Improve ESPHome abort messages for already-configured devices

Users often struggle to identify which ESPHome device is already configured—especially when replacing a device or renaming an existing one.
This PR improves the abort messages to include more helpful details, so users can pinpoint the conflicting device without needing to dig through the `core.config_entries` file manually.

* Update homeassistant/components/esphome/strings.json
This commit is contained in:
J. Nick Koston 2025-04-21 03:41:15 -10:00 committed by GitHub
parent 352ef0d009
commit 6698b3a1dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 98 additions and 16 deletions

View File

@ -306,7 +306,32 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
updates[CONF_HOST] = host
if port is not None:
updates[CONF_PORT] = port
self._abort_if_unique_id_configured(updates=updates)
self._abort_unique_id_configured_with_details(updates=updates)
@callback
def _abort_unique_id_configured_with_details(self, updates: dict[str, Any]) -> None:
"""Abort if unique_id is already configured with details."""
assert self.unique_id is not None
if not (
conflict_entry := self.hass.config_entries.async_entry_for_domain_unique_id(
self.handler, self.unique_id
)
):
return
assert conflict_entry.unique_id is not None
if updates:
error = "already_configured_updates"
else:
error = "already_configured_detailed"
self._abort_if_unique_id_configured(
updates=updates,
error=error,
description_placeholders={
"title": conflict_entry.title,
"name": conflict_entry.data.get(CONF_DEVICE_NAME, "unknown"),
"mac": format_mac(conflict_entry.unique_id),
},
)
async def async_step_mqtt(
self, discovery_info: MqttServiceInfo
@ -341,7 +366,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
# Check if already configured
await self.async_set_unique_id(mac_address)
self._abort_if_unique_id_configured(
self._abort_unique_id_configured_with_details(
updates={CONF_HOST: self._host, CONF_PORT: self._port}
)
@ -479,7 +504,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
data=self._reauth_entry.data | self._async_make_config_data(),
)
assert self._host is not None
self._abort_if_unique_id_configured(
self._abort_unique_id_configured_with_details(
updates={
CONF_HOST: self._host,
CONF_PORT: self._port,
@ -510,7 +535,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
if not (
unique_id_matches := (self.unique_id == self._reconfig_entry.unique_id)
):
self._abort_if_unique_id_configured(
self._abort_unique_id_configured_with_details(
updates={
CONF_HOST: self._host,
CONF_PORT: self._port,
@ -640,7 +665,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
mac_address = format_mac(self._device_info.mac_address)
await self.async_set_unique_id(mac_address, raise_on_progress=False)
if self.source not in (SOURCE_REAUTH, SOURCE_RECONFIGURE):
self._abort_if_unique_id_configured(
self._abort_unique_id_configured_with_details(
updates={
CONF_HOST: self._host,
CONF_PORT: self._port,

View File

@ -2,6 +2,8 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_configured_detailed": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`.",
"already_configured_updates": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`; the existing configuration will be updated with the validated data.",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"mdns_missing_mac": "Missing MAC address in mDNS properties.",

View File

@ -119,7 +119,12 @@ async def test_user_connection_updates_host(
data={CONF_HOST: "127.0.0.1", CONF_PORT: 80},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "unknown",
"mac": "11:22:33:44:55:aa",
}
assert entry.data[CONF_HOST] == "127.0.0.1"
@ -173,7 +178,12 @@ async def test_user_sets_unique_id(
{CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "test",
"name": "test",
"mac": "11:22:33:44:55:aa",
}
@pytest.mark.usefixtures("mock_zeroconf")
@ -645,7 +655,12 @@ async def test_discovery_already_configured(
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "unknown",
"mac": "11:22:33:44:55:aa",
}
async def test_discovery_duplicate_data(
@ -701,7 +716,12 @@ async def test_discovery_updates_unique_id(
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "unknown",
"mac": "11:22:33:44:55:aa",
}
assert entry.unique_id == "11:22:33:44:55:aa"
@ -1159,7 +1179,12 @@ async def test_discovery_dhcp_updates_host(
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "unknown",
"mac": "11:22:33:44:55:aa",
}
assert entry.data[CONF_HOST] == "192.168.43.184"
@ -1188,7 +1213,12 @@ async def test_discovery_dhcp_does_not_update_host_wrong_mac(
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert result["reason"] == "already_configured_detailed"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "unknown",
"mac": "11:22:33:44:55:aa",
}
# Mac was wrong, should not update
assert entry.data[CONF_HOST] == "192.168.43.183"
@ -1217,7 +1247,12 @@ async def test_discovery_dhcp_does_not_update_host_wrong_mac_bad_key(
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert result["reason"] == "already_configured_detailed"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "unknown",
"mac": "11:22:33:44:55:aa",
}
# Mac was wrong, should not update
assert entry.data[CONF_HOST] == "192.168.43.183"
@ -1246,7 +1281,12 @@ async def test_discovery_dhcp_does_not_update_host_missing_mac_bad_key(
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert result["reason"] == "already_configured_detailed"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "unknown",
"mac": "11:22:33:44:55:aa",
}
# Mac was missing, should not update
assert entry.data[CONF_HOST] == "192.168.43.183"
@ -1999,7 +2039,12 @@ async def test_reconfig_mac_used_by_other_entry(
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "test4",
"mac": "11:22:33:44:55:bb",
}
@pytest.mark.usefixtures("mock_zeroconf", "mock_setup_entry")

View File

@ -743,7 +743,12 @@ async def test_connection_aborted_wrong_device(
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "test",
"mac": "11:22:33:44:55:aa",
}
assert entry.data[CONF_HOST] == "192.168.43.184"
await hass.async_block_till_done()
assert len(new_info.mock_calls) == 2
@ -812,7 +817,12 @@ async def test_connection_aborted_wrong_device_same_name(
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert result["reason"] == "already_configured_updates"
assert result["description_placeholders"] == {
"title": "Mock Title",
"name": "test",
"mac": "11:22:33:44:55:aa",
}
assert entry.data[CONF_HOST] == "192.168.43.184"
await hass.async_block_till_done()
assert len(new_info.mock_calls) == 2